Team functionality (#459)
* add api enpoints for adding, removing, updating user to workspace and leaving workspace * feat: updates client site workspace settings * refactor and add domain setting ui in modal * move workspace user functionality to its own component * adds tests * fix linting * updates select input to FlatSelectInput * moves workspace user role edit to seperated component * move user adding to its own component * adds check to usure users exist before checking is admin * fix loading users * feat: invite user to team functionality * fix token coulmn * fix self host mode changes * tests for user invite * Refactor back-end * Rename variables * Improve some styling elements + refactor workspace settings * More styling * More UI polishing * More UI fixes * PHP linting * Implemented most of the logic for team-functionnality * Fix user avatar URL * WIP remove users on cancellation * Finished pricing for team functionality * Fix tests * Fix linting * Added pricing_enabled helper * Fix pricing_enabled shortcut * Debug CI * Disable pricing when testing --------- Co-authored-by: LL-Etiane <lukongleinyuyetiane@gmail.com> Co-authored-by: Lukong Etiane <83535251+LL-Etiane@users.noreply.github.com> Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
383fff7b2c
commit
90ff91b1e9
|
|
@ -13,11 +13,11 @@
|
||||||
|
|
||||||
namespace App\Models\Billing{
|
namespace App\Models\Billing{
|
||||||
/**
|
/**
|
||||||
* App\Models\Billing\Subscription
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $user_id
|
* @property int $user_id
|
||||||
* @property string $name
|
* @property string $type
|
||||||
* @property string $stripe_id
|
* @property string $stripe_id
|
||||||
* @property string $stripe_status
|
* @property string $stripe_status
|
||||||
* @property string|null $stripe_price
|
* @property string|null $stripe_price
|
||||||
|
|
@ -32,14 +32,12 @@ namespace App\Models\Billing{
|
||||||
* @property-read \App\Models\User|null $user
|
* @property-read \App\Models\User|null $user
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription active()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription active()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription canceled()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription canceled()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription cancelled()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription ended()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription ended()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription expiredTrial()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription expiredTrial()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription incomplete()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription incomplete()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notCanceled()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notCanceled()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notCancelled()
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notOnGracePeriod()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notOnGracePeriod()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notOnTrial()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription notOnTrial()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription onGracePeriod()
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription onGracePeriod()
|
||||||
|
|
@ -50,12 +48,12 @@ namespace App\Models\Billing{
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereEndsAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereEndsAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereName($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereQuantity($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereQuantity($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripeId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripeId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripePrice($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripePrice($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripeStatus($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereStripeStatus($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereTrialEndsAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereTrialEndsAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereType($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereUserId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Subscription whereUserId($value)
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,12 +62,12 @@ namespace App\Models\Billing{
|
||||||
|
|
||||||
namespace App\Models\Forms\AI{
|
namespace App\Models\Forms\AI{
|
||||||
/**
|
/**
|
||||||
* App\Models\Forms\AI\AiFormCompletion
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $form_prompt
|
* @property string $form_prompt
|
||||||
* @property string $status
|
* @property string $status
|
||||||
* @property mixed|null $result
|
* @property string|null $result
|
||||||
* @property string $ip
|
* @property string $ip
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
|
@ -89,7 +87,7 @@ namespace App\Models\Forms\AI{
|
||||||
|
|
||||||
namespace App\Models\Forms{
|
namespace App\Models\Forms{
|
||||||
/**
|
/**
|
||||||
* App\Models\Forms\Form
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $workspace_id
|
* @property int $workspace_id
|
||||||
|
|
@ -98,7 +96,6 @@ namespace App\Models\Forms{
|
||||||
* @property array $properties
|
* @property array $properties
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property bool $notifies
|
|
||||||
* @property string|null $description
|
* @property string|null $description
|
||||||
* @property string $submit_button_text
|
* @property string $submit_button_text
|
||||||
* @property bool $re_fillable
|
* @property bool $re_fillable
|
||||||
|
|
@ -109,43 +106,35 @@ namespace App\Models\Forms{
|
||||||
* @property bool $hide_title
|
* @property bool $hide_title
|
||||||
* @property string $submitted_text
|
* @property string $submitted_text
|
||||||
* @property string $dark_mode
|
* @property string $dark_mode
|
||||||
* @property string|null $webhook_url
|
|
||||||
* @property bool $send_submission_confirmation
|
|
||||||
* @property string|null $logo_picture
|
* @property string|null $logo_picture
|
||||||
* @property string|null $cover_picture
|
* @property string|null $cover_picture
|
||||||
* @property string|null $redirect_url
|
* @property string|null $redirect_url
|
||||||
* @property string|null $custom_code
|
* @property string|null $custom_code
|
||||||
* @property string|null $notification_emails
|
|
||||||
* @property string $theme
|
* @property string $theme
|
||||||
* @property array|null $database_fields_update
|
* @property array|null $database_fields_update
|
||||||
* @property string $width
|
* @property string $width
|
||||||
* @property bool $transparent_background
|
* @property bool $transparent_background
|
||||||
* @property \Illuminate\Support\Carbon|null $closes_at
|
* @property \Illuminate\Support\Carbon|null $closes_at
|
||||||
* @property string|null $closed_text
|
* @property string|null $closed_text
|
||||||
* @property string $notification_subject
|
|
||||||
* @property string $notification_body
|
|
||||||
* @property bool $notifications_include_submission
|
|
||||||
* @property bool $use_captcha
|
* @property bool $use_captcha
|
||||||
* @property bool $can_be_indexed
|
* @property bool $can_be_indexed
|
||||||
* @property string|null $password
|
* @property string|null $password
|
||||||
* @property string $notification_sender
|
* @property array $tags
|
||||||
* @property array|null $tags
|
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||||
* @property int $creator_id
|
* @property int $creator_id
|
||||||
* @property-read array|null $removed_properties
|
* @property-read array $removed_properties
|
||||||
* @property int|null $max_submissions_count
|
* @property int|null $max_submissions_count
|
||||||
* @property string|null $max_submissions_reached_text
|
* @property string|null $max_submissions_reached_text
|
||||||
* @property string|null $slack_webhook_url
|
|
||||||
* @property string $visibility
|
* @property string $visibility
|
||||||
* @property bool $editable_submissions
|
* @property bool $editable_submissions
|
||||||
* @property string|null $discord_webhook_url
|
|
||||||
* @property string $editable_submissions_button_text
|
* @property string $editable_submissions_button_text
|
||||||
* @property bool $confetti_on_submission
|
* @property bool $confetti_on_submission
|
||||||
* @property object $seo_meta
|
* @property object $seo_meta
|
||||||
* @property object|null $notification_settings
|
|
||||||
* @property bool $auto_save
|
* @property bool $auto_save
|
||||||
* @property string|null $custom_domain
|
* @property string|null $custom_domain
|
||||||
* @property bool $show_progress_bar
|
* @property bool $show_progress_bar
|
||||||
|
* @property string $size
|
||||||
|
* @property string $border_radius
|
||||||
* @property-read \App\Models\User $creator
|
* @property-read \App\Models\User $creator
|
||||||
* @property-read mixed $edit_url
|
* @property-read mixed $edit_url
|
||||||
* @property-read mixed $form_pending_submission_key
|
* @property-read mixed $form_pending_submission_key
|
||||||
|
|
@ -154,12 +143,11 @@ namespace App\Models\Forms{
|
||||||
* @property-read mixed $is_pro
|
* @property-read mixed $is_pro
|
||||||
* @property-read mixed $max_file_size
|
* @property-read mixed $max_file_size
|
||||||
* @property-read mixed $max_number_of_submissions_reached
|
* @property-read mixed $max_number_of_submissions_reached
|
||||||
* @property-read mixed $notifies_discord
|
|
||||||
* @property-read mixed $notifies_slack
|
|
||||||
* @property-read mixed $notifies_webhook
|
|
||||||
* @property-read mixed $share_url
|
* @property-read mixed $share_url
|
||||||
* @property-read int|null $submissions_count
|
* @property-read int|null $submissions_count
|
||||||
* @property-read int|null $views_count
|
* @property-read int|null $views_count
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Integration\FormIntegration> $integrations
|
||||||
|
* @property-read int|null $integrations_count
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormStatistic> $statistics
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormStatistic> $statistics
|
||||||
* @property-read int|null $statistics_count
|
* @property-read int|null $statistics_count
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormSubmission> $submissions
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormSubmission> $submissions
|
||||||
|
|
@ -173,6 +161,7 @@ namespace App\Models\Forms{
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|Form onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form query()
|
* @method static \Illuminate\Database\Eloquent\Builder|Form query()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereAutoSave($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereAutoSave($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereBorderRadius($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereCanBeIndexed($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereCanBeIndexed($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereClosedText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereClosedText($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereClosesAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereClosesAt($value)
|
||||||
|
|
@ -187,7 +176,6 @@ namespace App\Models\Forms{
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDatabaseFieldsUpdate($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDatabaseFieldsUpdate($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDeletedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDeletedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDescription($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDescription($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereDiscordWebhookUrl($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissions($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissions($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissionsButtonText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissionsButtonText($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereHideTitle($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereHideTitle($value)
|
||||||
|
|
@ -196,23 +184,15 @@ namespace App\Models\Forms{
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsCount($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsCount($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsReachedText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsReachedText($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNoBranding($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNoBranding($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationBody($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationEmails($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSender($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSettings($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSubject($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationsIncludeSubmission($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereNotifies($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form wherePassword($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form wherePassword($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereProperties($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereProperties($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillButtonText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillButtonText($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillable($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillable($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereRedirectUrl($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereRedirectUrl($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereRemovedProperties($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereRemovedProperties($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSendSubmissionConfirmation($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSeoMeta($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSeoMeta($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereShowProgressBar($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereShowProgressBar($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSlackWebhookUrl($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSize($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSlug($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSlug($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmitButtonText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmitButtonText($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmittedText($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmittedText($value)
|
||||||
|
|
@ -224,7 +204,6 @@ namespace App\Models\Forms{
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereUppercaseLabels($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereUppercaseLabels($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereUseCaptcha($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereUseCaptcha($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereVisibility($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereVisibility($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereWebhookUrl($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereWidth($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereWidth($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form whereWorkspaceId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Form whereWorkspaceId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Form withTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|Form withTrashed()
|
||||||
|
|
@ -235,7 +214,7 @@ namespace App\Models\Forms{
|
||||||
|
|
||||||
namespace App\Models\Forms{
|
namespace App\Models\Forms{
|
||||||
/**
|
/**
|
||||||
* App\Models\Forms\FormStatistic
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $form_id
|
* @property int $form_id
|
||||||
|
|
@ -255,7 +234,7 @@ namespace App\Models\Forms{
|
||||||
|
|
||||||
namespace App\Models\Forms{
|
namespace App\Models\Forms{
|
||||||
/**
|
/**
|
||||||
* App\Models\Forms\FormSubmission
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $form_id
|
* @property int $form_id
|
||||||
|
|
@ -277,7 +256,7 @@ namespace App\Models\Forms{
|
||||||
|
|
||||||
namespace App\Models\Forms{
|
namespace App\Models\Forms{
|
||||||
/**
|
/**
|
||||||
* App\Models\Forms\FormView
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $form_id
|
* @property int $form_id
|
||||||
|
|
@ -297,7 +276,7 @@ namespace App\Models\Forms{
|
||||||
|
|
||||||
namespace App\Models\Integration{
|
namespace App\Models\Integration{
|
||||||
/**
|
/**
|
||||||
* App\Models\Integration\FormIntegration
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $form_id
|
* @property int $form_id
|
||||||
|
|
@ -311,6 +290,8 @@ namespace App\Models\Integration{
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Integration\FormIntegrationsEvent> $events
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Integration\FormIntegrationsEvent> $events
|
||||||
* @property-read int|null $events_count
|
* @property-read int|null $events_count
|
||||||
* @property-read \App\Models\Forms\Form|null $form
|
* @property-read \App\Models\Forms\Form|null $form
|
||||||
|
* @property-read \App\Models\OAuthProvider|null $provider
|
||||||
|
* @method static \Database\Factories\Integration\FormIntegrationFactory factory($count = null, $state = [])
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration query()
|
* @method static \Illuminate\Database\Eloquent\Builder|FormIntegration query()
|
||||||
|
|
@ -329,7 +310,7 @@ namespace App\Models\Integration{
|
||||||
|
|
||||||
namespace App\Models\Integration{
|
namespace App\Models\Integration{
|
||||||
/**
|
/**
|
||||||
* App\Models\Integration\FormIntegrationsEvent
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $integration_id
|
* @property int $integration_id
|
||||||
|
|
@ -353,7 +334,7 @@ namespace App\Models\Integration{
|
||||||
|
|
||||||
namespace App\Models\Integration{
|
namespace App\Models\Integration{
|
||||||
/**
|
/**
|
||||||
* App\Models\Integration\FormZapierWebhook
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $form_id
|
* @property int $form_id
|
||||||
|
|
@ -380,7 +361,7 @@ namespace App\Models\Integration{
|
||||||
|
|
||||||
namespace App\Models{
|
namespace App\Models{
|
||||||
/**
|
/**
|
||||||
* App\Models\License
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $license_key
|
* @property string $license_key
|
||||||
|
|
@ -392,6 +373,7 @@ namespace App\Models{
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read int|null $custom_domain_limit_count
|
* @property-read int|null $custom_domain_limit_count
|
||||||
* @property-read int $max_file_size
|
* @property-read int $max_file_size
|
||||||
|
* @property-read int|null $max_users_count
|
||||||
* @property-read \App\Models\User|null $user
|
* @property-read \App\Models\User|null $user
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|License active()
|
* @method static \Illuminate\Database\Eloquent\Builder|License active()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|License newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|License newModelQuery()
|
||||||
|
|
@ -411,26 +393,33 @@ namespace App\Models{
|
||||||
|
|
||||||
namespace App\Models{
|
namespace App\Models{
|
||||||
/**
|
/**
|
||||||
* App\Models\OAuthProvider
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $user_id
|
* @property int $user_id
|
||||||
* @property string $provider
|
* @property \App\Integrations\OAuth\OAuthProviderService $provider
|
||||||
* @property string $provider_user_id
|
* @property string $provider_user_id
|
||||||
* @property string|null $access_token
|
* @property string|null $access_token
|
||||||
* @property string|null $refresh_token
|
* @property string|null $refresh_token
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property string|null $email
|
||||||
|
* @property string|null $name
|
||||||
|
* @property \Illuminate\Support\Carbon|null $token_expires_at
|
||||||
* @property-read \App\Models\User $user
|
* @property-read \App\Models\User $user
|
||||||
|
* @method static \Database\Factories\OAuthProviderFactory factory($count = null, $state = [])
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider query()
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider query()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereAccessToken($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereAccessToken($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereEmail($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereName($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProvider($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProvider($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProviderUserId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProviderUserId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereRefreshToken($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereRefreshToken($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereTokenExpiresAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUserId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUserId($value)
|
||||||
*/
|
*/
|
||||||
|
|
@ -439,7 +428,7 @@ namespace App\Models{
|
||||||
|
|
||||||
namespace App\Models{
|
namespace App\Models{
|
||||||
/**
|
/**
|
||||||
* App\Models\Template
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
|
@ -482,7 +471,7 @@ namespace App\Models{
|
||||||
|
|
||||||
namespace App\Models{
|
namespace App\Models{
|
||||||
/**
|
/**
|
||||||
* App\Models\User
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
|
|
@ -520,8 +509,10 @@ namespace App\Models{
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Workspace> $workspaces
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Workspace> $workspaces
|
||||||
* @property-read int|null $workspaces_count
|
* @property-read int|null $workspaces_count
|
||||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|User hasExpiredGenericTrial()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|User onGenericTrial()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User query()
|
* @method static \Illuminate\Database\Eloquent\Builder|User query()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
|
||||||
|
|
@ -543,7 +534,64 @@ namespace App\Models{
|
||||||
|
|
||||||
namespace App\Models{
|
namespace App\Models{
|
||||||
/**
|
/**
|
||||||
* App\Models\Workspace
|
*
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $workspace_id
|
||||||
|
* @property string $email
|
||||||
|
* @property string $role
|
||||||
|
* @property string $token
|
||||||
|
* @property string $status
|
||||||
|
* @property string|null $valid_until
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\Workspace|null $workspace
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite notExpired()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite pending()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereEmail($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereRole($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereStatus($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereToken($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereValidUntil($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserInvite whereWorkspaceId($value)
|
||||||
|
*/
|
||||||
|
class UserInvite extends \Eloquent {}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace App\Models{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $workspace_id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property string $role
|
||||||
|
* @property-read \App\Models\User $user
|
||||||
|
* @property-read \App\Models\Workspace $workspace
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereRole($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereUserId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserWorkspace whereWorkspaceId($value)
|
||||||
|
*/
|
||||||
|
class UserWorkspace extends \Eloquent {}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace App\Models{
|
||||||
|
/**
|
||||||
|
*
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
|
@ -557,10 +605,15 @@ namespace App\Models{
|
||||||
* @property-read mixed $is_enterprise
|
* @property-read mixed $is_enterprise
|
||||||
* @property-read mixed $is_pro
|
* @property-read mixed $is_pro
|
||||||
* @property-read mixed $is_risky
|
* @property-read mixed $is_risky
|
||||||
|
* @property-read mixed $is_trialing
|
||||||
* @property-read mixed $max_file_size
|
* @property-read mixed $max_file_size
|
||||||
|
* @property-read mixed $max_user_count_limit
|
||||||
* @property-read mixed $submissions_count
|
* @property-read mixed $submissions_count
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\UserInvite> $invites
|
||||||
|
* @property-read int|null $invites_count
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
||||||
* @property-read int|null $users_count
|
* @property-read int|null $users_count
|
||||||
|
* @method static \Database\Factories\WorkspaceFactory factory($count = null, $state = [])
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Workspace newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Workspace newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Workspace newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Workspace newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Workspace query()
|
* @method static \Illuminate\Database\Eloquent\Builder|Workspace query()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Events;
|
namespace App\Events\Billing;
|
||||||
|
|
||||||
use App\Models\Billing\Subscription;
|
use App\Models\Billing\Subscription;
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events\Billing;
|
||||||
|
|
||||||
|
use App\Models\Billing\Subscription;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class SubscriptionUpdated
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithSockets;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public Subscription $subscription)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Resources\UserResource;
|
use App\Http\Resources\UserResource;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\UserInvite;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
|
|
@ -62,6 +63,7 @@ class RegisterController extends Controller
|
||||||
'hear_about_us' => 'required|string',
|
'hear_about_us' => 'required|string',
|
||||||
'agree_terms' => ['required', Rule::in([true])],
|
'agree_terms' => ['required', Rule::in([true])],
|
||||||
'appsumo_license' => ['nullable'],
|
'appsumo_license' => ['nullable'],
|
||||||
|
'invite_token' => ['nullable', 'string'],
|
||||||
], [
|
], [
|
||||||
'agree_terms' => 'Please agree with the terms and conditions.',
|
'agree_terms' => 'Please agree with the terms and conditions.',
|
||||||
]);
|
]);
|
||||||
|
|
@ -69,15 +71,11 @@ class RegisterController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new user instance after a valid registration.
|
* Create a new user instance after a valid registration.
|
||||||
*
|
|
||||||
* @return \App\User
|
|
||||||
*/
|
*/
|
||||||
protected function create(array $data)
|
protected function create(array $data)
|
||||||
{
|
{
|
||||||
$workspace = Workspace::create([
|
$this->checkRegistrationAllowed($data);
|
||||||
'name' => 'My Workspace',
|
[$workspace, $role] = $this->getWorkspaceAndRole($data);
|
||||||
'icon' => '🧪',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
|
|
@ -89,7 +87,7 @@ class RegisterController extends Controller
|
||||||
// Add relation with user
|
// Add relation with user
|
||||||
$user->workspaces()->sync([
|
$user->workspaces()->sync([
|
||||||
$workspace->id => [
|
$workspace->id => [
|
||||||
'role' => 'admin',
|
'role' => $role,
|
||||||
],
|
],
|
||||||
], false);
|
], false);
|
||||||
|
|
||||||
|
|
@ -97,4 +95,45 @@ class RegisterController extends Controller
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkRegistrationAllowed(array $data)
|
||||||
|
{
|
||||||
|
if (config('app.self_hosted') && !array_key_exists('invite_token', $data)) {
|
||||||
|
response()->json(['message' => 'Registration is not allowed in self host mode'], 400)->throwResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getWorkspaceAndRole(array $data)
|
||||||
|
{
|
||||||
|
if (!array_key_exists('invite_token', $data)) {
|
||||||
|
return [
|
||||||
|
Workspace::create([
|
||||||
|
'name' => 'My Workspace',
|
||||||
|
'icon' => '🧪',
|
||||||
|
]),
|
||||||
|
User::ROLE_ADMIN
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInvite = UserInvite::where('email', $data['email'])
|
||||||
|
->where('token', $data['invite_token'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$userInvite) {
|
||||||
|
response()->json(['message' => 'Invite token is invalid.'], 400)->throwResponse();
|
||||||
|
}
|
||||||
|
if ($userInvite->hasExpired()) {
|
||||||
|
response()->json(['message' => 'Invite token has expired.'], 400)->throwResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
||||||
|
response()->json(['message' => 'Invite is already accepted.'], 400)->throwResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInvite->markAsAccepted();
|
||||||
|
return [
|
||||||
|
$userInvite->workspace,
|
||||||
|
$userInvite->role,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Requests\Subscriptions\UpdateStripeDetailsRequest;
|
use App\Http\Requests\Subscriptions\UpdateStripeDetailsRequest;
|
||||||
use Illuminate\Support\Facades\App;
|
use App\Service\BillingHelper;
|
||||||
|
use App\Service\UserHelper;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Laravel\Cashier\Subscription;
|
use Laravel\Cashier\Subscription;
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ class SubscriptionController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
$checkoutBuilder = $user
|
$checkoutBuilder = $user
|
||||||
->newSubscription($pricing, $this->getPricing($pricing)[$plan])
|
->newSubscription($pricing, BillingHelper::getPricing($pricing)[$plan])
|
||||||
->allowPromotionCodes();
|
->allowPromotionCodes();
|
||||||
|
|
||||||
if ($trial != null) {
|
if ($trial != null) {
|
||||||
|
|
@ -60,10 +61,18 @@ class SubscriptionController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUsersCount()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
return [
|
||||||
|
'count' => (new UserHelper(Auth::user()))->getActiveMembersCount() - 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function updateStripeDetails(UpdateStripeDetailsRequest $request)
|
public function updateStripeDetails(UpdateStripeDetailsRequest $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
if (! $user->hasStripeId()) {
|
if (!$user->hasStripeId()) {
|
||||||
$user->createAsStripeCustomer();
|
$user->createAsStripeCustomer();
|
||||||
}
|
}
|
||||||
$user->updateStripeCustomer([
|
$user->updateStripeCustomer([
|
||||||
|
|
@ -79,7 +88,7 @@ class SubscriptionController extends Controller
|
||||||
public function billingPortal()
|
public function billingPortal()
|
||||||
{
|
{
|
||||||
$this->middleware('auth');
|
$this->middleware('auth');
|
||||||
if (! Auth::user()->has_customer_id) {
|
if (!Auth::user()->has_customer_id) {
|
||||||
return $this->error([
|
return $this->error([
|
||||||
'message' => 'Please subscribe before accessing your billing portal.',
|
'message' => 'Please subscribe before accessing your billing portal.',
|
||||||
]);
|
]);
|
||||||
|
|
@ -89,9 +98,4 @@ class SubscriptionController extends Controller
|
||||||
'portal_url' => Auth::user()->billingPortalUrl(front_url('/home')),
|
'portal_url' => Auth::user()->billingPortalUrl(front_url('/home')),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPricing($product = 'default')
|
|
||||||
{
|
|
||||||
return App::environment() == 'production' ? config('pricing.production.'.$product.'.pricing') : config('pricing.test.'.$product.'.pricing');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\UserInvite;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Service\WorkspaceHelper;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserInviteController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listInvites(Request $request, $workspaceId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('view', $workspace);
|
||||||
|
|
||||||
|
return (new WorkspaceHelper($workspace))->getAllInvites();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resendInvite($workspaceId, $inviteId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('adminAction', $workspace);
|
||||||
|
$userInvite = $workspace->invites()->find($inviteId);
|
||||||
|
if (!$userInvite) {
|
||||||
|
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
||||||
|
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInvite->sendEmail();
|
||||||
|
|
||||||
|
return $this->success(['message' => 'Invite email resent successfully.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelInvite($workspaceId, $inviteId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('adminAction', $workspace);
|
||||||
|
$userInvite = $workspace->invites()->find($inviteId);
|
||||||
|
if (!$userInvite) {
|
||||||
|
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
||||||
|
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInvite->delete();
|
||||||
|
|
||||||
|
return $this->success(['message' => 'Invite deleted successfully.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Controllers\Webhook;
|
namespace App\Http\Controllers\Webhook;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Jobs\Billing\RemoveWorkspaceGuests;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
@ -56,7 +57,8 @@ class AppSumoController extends Controller
|
||||||
|
|
||||||
private function handleDeactivateEvent($request)
|
private function handleDeactivateEvent($request)
|
||||||
{
|
{
|
||||||
$this->deactivateLicense($request->license_key);
|
$license = $this->deactivateLicense($request->license_key);
|
||||||
|
RemoveWorkspaceGuests::dispatch($license->user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createLicense(array $licenseData): License
|
private function createLicense(array $licenseData): License
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,17 @@ class StripeController extends WebhookController
|
||||||
|
|
||||||
$subscription->type = $subscription->type ?? $data['metadata']['name'] ?? $this->newSubscriptionName($payload);
|
$subscription->type = $subscription->type ?? $data['metadata']['name'] ?? $this->newSubscriptionName($payload);
|
||||||
|
|
||||||
$firstItem = $data['items']['data'][0];
|
$mainItem = $this->getMainSubscriptionLineItem($data['items']['data']);
|
||||||
$isSinglePrice = count($data['items']['data']) === 1;
|
$isSinglePrice = count($data['items']['data']) === 1;
|
||||||
|
|
||||||
// Price...
|
// Price...
|
||||||
$subscription->stripe_price = $isSinglePrice ? $firstItem['price']['id'] : null;
|
$subscription->stripe_price = $isSinglePrice ? $mainItem['price']['id'] : null;
|
||||||
|
|
||||||
// Type - previously (Name)
|
// Type - previously (Name)
|
||||||
$subscription->type = $this->getSubscriptionName($data['plan']['product']);
|
$subscription->type = $this->getSubscriptionName($mainItem['price']['product']);
|
||||||
|
|
||||||
// Quantity...
|
// Quantity...
|
||||||
$subscription->quantity = $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null;
|
$subscription->quantity = $isSinglePrice && isset($mainItem['quantity']) ? $mainItem['quantity'] : null;
|
||||||
|
|
||||||
// Trial ending date...
|
// Trial ending date...
|
||||||
if (isset($data['trial_end'])) {
|
if (isset($data['trial_end'])) {
|
||||||
|
|
@ -115,6 +115,13 @@ class StripeController extends WebhookController
|
||||||
return $this->successMethod();
|
return $this->successMethod();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getMainSubscriptionLineItem(array $items)
|
||||||
|
{
|
||||||
|
return collect($items)->first(function ($item) {
|
||||||
|
return in_array($this->getSubscriptionName($item['price']['product']), ['default']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function getSubscriptionName(string $stripeProductId)
|
private function getSubscriptionName(string $stripeProductId)
|
||||||
{
|
{
|
||||||
$config = App::environment() == 'production' ? config('pricing.production') : config('pricing.test');
|
$config = App::environment() == 'production' ? config('pricing.production') : config('pricing.test');
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers;
|
||||||
use App\Http\Requests\Workspace\CustomDomainRequest;
|
use App\Http\Requests\Workspace\CustomDomainRequest;
|
||||||
use App\Http\Resources\WorkspaceResource;
|
use App\Http\Resources\WorkspaceResource;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Service\WorkspaceHelper;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
|
@ -23,14 +22,6 @@ class WorkspaceController extends Controller
|
||||||
return WorkspaceResource::collection(Auth::user()->workspaces);
|
return WorkspaceResource::collection(Auth::user()->workspaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listUsers(Request $request, $workspaceId)
|
|
||||||
{
|
|
||||||
$workspace = Workspace::findOrFail($workspaceId);
|
|
||||||
$this->authorize('view', $workspace);
|
|
||||||
|
|
||||||
return (new WorkspaceHelper($workspace))->getAllUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveCustomDomain(CustomDomainRequest $request)
|
public function saveCustomDomain(CustomDomainRequest $request)
|
||||||
{
|
{
|
||||||
$request->workspace->custom_domains = $request->customDomains;
|
$request->workspace->custom_domains = $request->customDomains;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Jobs\Billing\WorkspaceUsersUpdated;
|
||||||
|
use App\Models\UserInvite;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Service\WorkspaceHelper;
|
||||||
|
|
||||||
|
class WorkspaceUserController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listUsers(Request $request, $workspaceId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('view', $workspace);
|
||||||
|
|
||||||
|
return (new WorkspaceHelper($workspace))->getAllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUser(Request $request, $workspaceId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('inviteUser', $workspace);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'email' => 'required|email',
|
||||||
|
'role' => 'required|in:admin,user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('email', $request->email)->first();
|
||||||
|
if (!$user) {
|
||||||
|
return $this->inviteUser($workspace, $request->email, $request->role);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workspace->users->contains($user->id)) {
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'User is already in workspace.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User found - add user to workspace
|
||||||
|
$workspace->users()->sync([
|
||||||
|
$user->id => [
|
||||||
|
'role' => $request->role,
|
||||||
|
],
|
||||||
|
], false);
|
||||||
|
WorkspaceUsersUpdated::dispatch($workspace);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'User has been successfully added to workspace.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function inviteUser(Workspace $workspace, string $email, string $role)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
UserInvite::where('email', $email)
|
||||||
|
->where('workspace_id', $workspace->id)
|
||||||
|
->notExpired()
|
||||||
|
->pending()
|
||||||
|
->exists()) {
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'User has already been invited.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send new invite
|
||||||
|
UserInvite::inviteUser($email, $role, $workspace, now()->addDays(7));
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'Registration invitation email sent to user.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateUserRole(Request $request, $workspaceId, $userId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$user = User::findOrFail($userId);
|
||||||
|
$this->authorize('adminAction', $workspace);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'role' => 'required|in:admin,user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$workspace->users()->sync([
|
||||||
|
$user->id => [
|
||||||
|
'role' => $request->role,
|
||||||
|
],
|
||||||
|
], false);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'User role changed successfully.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUser(Request $request, $workspaceId, $userId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('adminAction', $workspace);
|
||||||
|
|
||||||
|
$workspace->users()->detach($userId);
|
||||||
|
WorkspaceUsersUpdated::dispatch($workspace);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'User removed from workspace successfully.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveWorkspace(Request $request, $workspaceId)
|
||||||
|
{
|
||||||
|
$workspace = Workspace::findOrFail($workspaceId);
|
||||||
|
$this->authorize('view', $workspace);
|
||||||
|
|
||||||
|
$workspace->users()->detach($request->user()->id);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'You have left the workspace successfully.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Billing;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RemoveWorkspaceGuests implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithQueue;
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public User $user)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
// If pricing not enabled
|
||||||
|
if (!pricing_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->user->is_subscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is not subscribed anymore - remove guests
|
||||||
|
$this->user->workspaces->each(function (Workspace $workspace) {
|
||||||
|
// Flush workspace cache to be sure we have the latest data
|
||||||
|
$workspace->flush();
|
||||||
|
if ($workspace->is_pro) {
|
||||||
|
// Another user still has pro subscription
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach all users from the workspace (except the owner)
|
||||||
|
foreach ($workspace->users()->where('users.id', '!=', $this->user->id)->get() as $user) {
|
||||||
|
\Log::info('Detaching user from workspace', [
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'workspace_name' => $workspace->name,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_email' => $user->email,
|
||||||
|
]);
|
||||||
|
$workspace->users()->detach($user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Billing;
|
||||||
|
|
||||||
|
use App\Models\Billing\Subscription;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Service\BillingHelper;
|
||||||
|
use App\Service\UserHelper;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Laravel\Cashier\Cashier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update subscription with extra users when workspace users are updated.
|
||||||
|
*/
|
||||||
|
class WorkspaceUsersUpdated implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithQueue;
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public Workspace $workspace)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
// If self-hosted, no need to update billing
|
||||||
|
if (!pricing_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @var User $billingOwner
|
||||||
|
*/
|
||||||
|
$billingOwner = $this->workspace->billingOwners()->first();
|
||||||
|
|
||||||
|
if (!$billingOwner || !$billingOwner->is_subscribed) {
|
||||||
|
// If somehow billing owner is not found or not subscribed, no need to update billing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($billingOwner->activeLicense()) {
|
||||||
|
// No need to update billing if user has a fixed license
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now update the subscription accordingly
|
||||||
|
$subscription = $billingOwner->subscription();
|
||||||
|
$totalUsersCount = (new UserHelper($billingOwner))->getActiveMembersCount() - 1;
|
||||||
|
$this->updateSubscriptionWithExtraUsers($subscription, $totalUsersCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateSubscriptionWithExtraUsers(Subscription $subscription, int $quantity): void
|
||||||
|
{
|
||||||
|
$stripe = Cashier::stripe();
|
||||||
|
$extraUserPricing = BillingHelper::getPricing('extra_user');
|
||||||
|
$stripeSub = $subscription->asStripeSubscription();
|
||||||
|
$lineItems = collect($stripeSub->items);
|
||||||
|
|
||||||
|
// Make sure Stripe sub has the right pro-rating settings
|
||||||
|
$stripe->subscriptions->update($stripeSub->id, [
|
||||||
|
'proration_behavior' => 'always_invoice',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Main sub info
|
||||||
|
$mainSubscriptionItem = $this->getLineItem($lineItems, 'default');
|
||||||
|
$subscriptionInterval = BillingHelper::getLineItemInterval($mainSubscriptionItem);
|
||||||
|
|
||||||
|
$extraUserLineItem = $this->getLineItem($lineItems, 'extra_user');
|
||||||
|
if ($extraUserLineItem) {
|
||||||
|
$stripe->subscriptionItems->update(
|
||||||
|
$extraUserLineItem->id,
|
||||||
|
['quantity' => $quantity]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$stripeSub->items->create([
|
||||||
|
'price' => $extraUserPricing[$subscriptionInterval],
|
||||||
|
'quantity' => $quantity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLineItem(Collection $lineItems, string $productName)
|
||||||
|
{
|
||||||
|
$productId = BillingHelper::getProductId($productName);
|
||||||
|
if (!$productId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $lineItems->first(function ($lineItem) use ($productId) {
|
||||||
|
return $lineItem->price->product === $productId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners\Billing;
|
||||||
|
|
||||||
use App\Events\SubscriptionCreated;
|
use App\Events\Billing\SubscriptionCreated;
|
||||||
|
use App\Jobs\Billing\WorkspaceUsersUpdated;
|
||||||
|
|
||||||
class HandleSubscriptionCreated
|
class HandleSubscriptionCreated
|
||||||
{
|
{
|
||||||
|
|
@ -22,5 +23,9 @@ class HandleSubscriptionCreated
|
||||||
$workspace->forms()->update(['no_branding' => true]);
|
$workspace->forms()->update(['no_branding' => true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update pricing (number of users)
|
||||||
|
if ($workspace = $user->workspaces()->first()) {
|
||||||
|
WorkspaceUsersUpdated::dispatch($workspace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners\Billing;
|
||||||
|
|
||||||
|
use App\Events\Billing\SubscriptionUpdated;
|
||||||
|
use App\Jobs\Billing\RemoveWorkspaceGuests;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
|
class RemoveWorkspaceGuestsIfNeeded implements ShouldQueue
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*/
|
||||||
|
public function handle(SubscriptionUpdated $event): void
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Subscription $subscription
|
||||||
|
*/
|
||||||
|
$subscription = $event->subscription;
|
||||||
|
if (!$subscription->valid()) {
|
||||||
|
RemoveWorkspaceGuests::dispatch($event->subscription->user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\UserInvite;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class UserInvitationEmail extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @param string $workspaceName
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(public UserInvite $invite)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
$workspaceName = $this->invite->workspace->name;
|
||||||
|
return $this
|
||||||
|
->markdown('mail.user.invitation', [
|
||||||
|
'workspaceName' => $workspaceName,
|
||||||
|
'inviteLink' => $this->invite->getLink(),
|
||||||
|
])->subject('You are invited to join ' . $workspaceName . ' on OpnForm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
namespace App\Models\Billing;
|
namespace App\Models\Billing;
|
||||||
|
|
||||||
use App\Events\SubscriptionCreated;
|
use App\Events\Billing\SubscriptionCreated;
|
||||||
|
use App\Events\Billing\SubscriptionUpdated;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Laravel\Cashier\Subscription as CashierSubscription;
|
use Laravel\Cashier\Subscription as CashierSubscription;
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ class Subscription extends CashierSubscription
|
||||||
|
|
||||||
protected $dispatchesEvents = [
|
protected $dispatchesEvents = [
|
||||||
'created' => SubscriptionCreated::class,
|
'created' => SubscriptionCreated::class,
|
||||||
|
'updated' => SubscriptionUpdated::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function booted(): void
|
public static function booted(): void
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
class License extends Model
|
class License extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
public const STATUS_ACTIVE = 'active';
|
|
||||||
|
|
||||||
|
public const STATUS_ACTIVE = 'active';
|
||||||
public const STATUS_INACTIVE = 'inactive';
|
public const STATUS_INACTIVE = 'inactive';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -32,6 +32,11 @@ class License extends Model
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
return $this->status === self::STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
public function scopeActive($query)
|
public function scopeActive($query)
|
||||||
{
|
{
|
||||||
return $query->where('status', self::STATUS_ACTIVE);
|
return $query->where('status', self::STATUS_ACTIVE);
|
||||||
|
|
@ -55,6 +60,15 @@ class License extends Model
|
||||||
][$this->meta['tier']];
|
][$this->meta['tier']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMaxUsersLimitCountAttribute(): ?int
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => 1,
|
||||||
|
2 => 5,
|
||||||
|
3 => null,
|
||||||
|
][$this->meta['tier']];
|
||||||
|
}
|
||||||
|
|
||||||
public static function booted(): void
|
public static function booted(): void
|
||||||
{
|
{
|
||||||
static::saved(function (License $license) {
|
static::saved(function (License $license) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ class User extends Authenticatable implements JWTSubject
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
|
public const ROLE_ADMIN = 'admin';
|
||||||
|
public const ROLE_USER = 'user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
|
|
@ -80,7 +83,7 @@ class User extends Authenticatable implements JWTSubject
|
||||||
{
|
{
|
||||||
return vsprintf('https://www.gravatar.com/avatar/%s.jpg?s=200&d=%s', [
|
return vsprintf('https://www.gravatar.com/avatar/%s.jpg?s=200&d=%s', [
|
||||||
md5(strtolower($this->email)),
|
md5(strtolower($this->email)),
|
||||||
$this->name ? urlencode("https://ui-avatars.com/api/$this->name") : 'mp',
|
$this->name ? urlencode("https://ui-avatars.com/api/$this->name.jpg") : 'mp',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,6 +238,8 @@ class User extends Authenticatable implements JWTSubject
|
||||||
foreach ($user->workspaces as $workspace) {
|
foreach ($user->workspaces as $workspace) {
|
||||||
if ($workspace->users()->count() == 1) {
|
if ($workspace->users()->count() == 1) {
|
||||||
$workspace->delete();
|
$workspace->delete();
|
||||||
|
} else {
|
||||||
|
$workspace->users()->detach($user->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Jobs\Billing\WorkspaceUsersUpdated;
|
||||||
|
use App\Mail\UserInvitationEmail;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class UserInvite extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const PENDING_STATUS = 'pending';
|
||||||
|
public const ACCEPTED_STATUS = 'accepted';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'email',
|
||||||
|
'role',
|
||||||
|
'workspace_id',
|
||||||
|
'valid_until',
|
||||||
|
'status',
|
||||||
|
'token',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function inviteUser(
|
||||||
|
string $email,
|
||||||
|
string $role,
|
||||||
|
Workspace $workspace,
|
||||||
|
Carbon $validUntil = null
|
||||||
|
): self {
|
||||||
|
// Generate a token
|
||||||
|
do {
|
||||||
|
$token = Str::random(100);
|
||||||
|
} while (UserInvite::where('token', $token)->exists());
|
||||||
|
|
||||||
|
$invite = self::create([
|
||||||
|
'email' => $email,
|
||||||
|
'role' => $role,
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'valid_until' => $validUntil ?? now()->addDays(7),
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
|
$invite->sendEmail();
|
||||||
|
return $invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLink()
|
||||||
|
{
|
||||||
|
return front_url('/register?email=' . urlencode($this->email) . '&invite_token=' . urlencode($this->token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasExpired()
|
||||||
|
{
|
||||||
|
return Carbon::parse($this->valid_until)->isPast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function workspace()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Workspace::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsAccepted()
|
||||||
|
{
|
||||||
|
$this->update(['status' => self::ACCEPTED_STATUS]);
|
||||||
|
WorkspaceUsersUpdated::dispatch($this->workspace);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendEmail()
|
||||||
|
{
|
||||||
|
Mail::to($this->email)->send(new UserInvitationEmail($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeNotExpired($query)
|
||||||
|
{
|
||||||
|
return $query->where('valid_until', '>', now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopePending($query)
|
||||||
|
{
|
||||||
|
return $query->where('status', self::PENDING_STATUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserWorkspace extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $table = 'user_workspace';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'workspace_id',
|
||||||
|
'role',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function workspace()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Workspace::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Models\Traits\CachableAttributes;
|
||||||
use App\Models\Traits\CachesAttributes;
|
use App\Models\Traits\CachesAttributes;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class Workspace extends Model implements CachableAttributes
|
class Workspace extends Model implements CachableAttributes
|
||||||
{
|
{
|
||||||
|
|
@ -50,7 +51,7 @@ class Workspace extends Model implements CachableAttributes
|
||||||
|
|
||||||
public function getMaxFileSizeAttribute()
|
public function getMaxFileSizeAttribute()
|
||||||
{
|
{
|
||||||
if (is_null(config('cashier.key'))) {
|
if (!pricing_enabled()) {
|
||||||
return self::MAX_FILE_SIZE_PRO;
|
return self::MAX_FILE_SIZE_PRO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +74,7 @@ class Workspace extends Model implements CachableAttributes
|
||||||
|
|
||||||
public function getCustomDomainCountLimitAttribute()
|
public function getCustomDomainCountLimitAttribute()
|
||||||
{
|
{
|
||||||
if (is_null(config('cashier.key'))) {
|
if (!pricing_enabled()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ class Workspace extends Model implements CachableAttributes
|
||||||
|
|
||||||
public function getIsProAttribute()
|
public function getIsProAttribute()
|
||||||
{
|
{
|
||||||
if (is_null(config('cashier.key'))) {
|
if (!pricing_enabled()) {
|
||||||
return true; // If no paid plan so TRUE for ALL
|
return true; // If no paid plan so TRUE for ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +114,7 @@ class Workspace extends Model implements CachableAttributes
|
||||||
|
|
||||||
public function getIsTrialingAttribute()
|
public function getIsTrialingAttribute()
|
||||||
{
|
{
|
||||||
if (is_null(config('cashier.key'))) {
|
if (!pricing_enabled()) {
|
||||||
return false; // If no paid plan so FALSE for ALL
|
return false; // If no paid plan so FALSE for ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +132,7 @@ class Workspace extends Model implements CachableAttributes
|
||||||
|
|
||||||
public function getIsEnterpriseAttribute()
|
public function getIsEnterpriseAttribute()
|
||||||
{
|
{
|
||||||
if (is_null(config('cashier.key'))) {
|
if (!pricing_enabled()) {
|
||||||
return true; // If no paid plan so TRUE for ALL
|
return true; // If no paid plan so TRUE for ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,11 +182,21 @@ class Workspace extends Model implements CachableAttributes
|
||||||
return $this->belongsToMany(User::class);
|
return $this->belongsToMany(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function invites()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserInvite::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function owners()
|
public function owners()
|
||||||
{
|
{
|
||||||
return $this->users()->wherePivot('role', 'admin');
|
return $this->users()->wherePivot('role', 'admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function billingOwners(): Collection
|
||||||
|
{
|
||||||
|
return $this->owners->filter(fn ($owner) => $owner->is_subscribed);
|
||||||
|
}
|
||||||
|
|
||||||
public function forms()
|
public function forms()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Form::class);
|
return $this->hasMany(Form::class);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
|
use App\Models\UserWorkspace;
|
||||||
|
use App\Service\UserHelper;
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
class WorkspacePolicy
|
class WorkspacePolicy
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +60,7 @@ class WorkspacePolicy
|
||||||
*/
|
*/
|
||||||
public function delete(User $user, Workspace $workspace)
|
public function delete(User $user, Workspace $workspace)
|
||||||
{
|
{
|
||||||
return ! $workspace->owners->where('id', $user->id)->isEmpty() && $user->workspaces()->count() > 1;
|
return !$workspace->owners->where('id', $user->id)->isEmpty() && $user->workspaces()->count() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,4 +82,44 @@ class WorkspacePolicy
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function inviteUser(User $user, Workspace $workspace)
|
||||||
|
{
|
||||||
|
if (!$this->adminAction($user, $workspace)) {
|
||||||
|
return Response::deny('You need to be an admin of this workspace to do this.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If self-hosted, allow
|
||||||
|
if (!pricing_enabled()) {
|
||||||
|
return Response::allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$workspace->is_pro) {
|
||||||
|
return Response::deny('You need a Pro subscription to invite a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case of special license, check license limit
|
||||||
|
$billingOwner = $workspace->billingOwners()->first();
|
||||||
|
if ($license = $billingOwner->activeLicense()) {
|
||||||
|
$userActiveMembers = (new UserHelper($billingOwner))->getActiveMembersCount();
|
||||||
|
if ($userActiveMembers >= $license->max_users_limit_count) {
|
||||||
|
return Response::deny('You have reached the maximum number of users allowed with your license.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user is an admin in the workspace.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function adminAction(User $user, Workspace $workspace)
|
||||||
|
{
|
||||||
|
$userWorkspace = UserWorkspace::where('user_id', $user->id)
|
||||||
|
->where('workspace_id', $workspace->id)
|
||||||
|
->first();
|
||||||
|
return $userWorkspace && $userWorkspace->role === 'admin';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Events\Billing\SubscriptionCreated;
|
||||||
|
use App\Events\Billing\SubscriptionUpdated;
|
||||||
use App\Events\Forms\FormSubmitted;
|
use App\Events\Forms\FormSubmitted;
|
||||||
use App\Events\Models\FormCreated;
|
use App\Events\Models\FormCreated;
|
||||||
use App\Events\Models\FormIntegrationCreated;
|
use App\Events\Models\FormIntegrationCreated;
|
||||||
use App\Events\Models\FormIntegrationsEventCreated;
|
use App\Events\Models\FormIntegrationsEventCreated;
|
||||||
use App\Events\SubscriptionCreated;
|
use App\Listeners\Billing\HandleSubscriptionCreated;
|
||||||
|
use App\Listeners\Billing\RemoveWorkspaceGuestsIfNeeded;
|
||||||
use App\Listeners\Forms\FormCreationConfirmation;
|
use App\Listeners\Forms\FormCreationConfirmation;
|
||||||
use App\Listeners\Forms\FormIntegrationCreatedHandler;
|
use App\Listeners\Forms\FormIntegrationCreatedHandler;
|
||||||
use App\Listeners\Forms\FormIntegrationsEventListener;
|
use App\Listeners\Forms\FormIntegrationsEventListener;
|
||||||
use App\Listeners\Forms\NotifyFormSubmission;
|
use App\Listeners\Forms\NotifyFormSubmission;
|
||||||
use App\Listeners\HandleSubscriptionCreated;
|
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
@ -40,8 +42,11 @@ class EventServiceProvider extends ServiceProvider
|
||||||
FormIntegrationsEventListener::class,
|
FormIntegrationsEventListener::class,
|
||||||
],
|
],
|
||||||
SubscriptionCreated::class => [
|
SubscriptionCreated::class => [
|
||||||
HandleSubscriptionCreated::class
|
HandleSubscriptionCreated::class,
|
||||||
],
|
],
|
||||||
|
SubscriptionUpdated::class => [
|
||||||
|
RemoveWorkspaceGuestsIfNeeded::class
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Stripe\SubscriptionItem;
|
||||||
|
|
||||||
|
class BillingHelper
|
||||||
|
{
|
||||||
|
public static function getPricing($productName = 'default')
|
||||||
|
{
|
||||||
|
return App::environment() == 'production' ?
|
||||||
|
config('pricing.production.' . $productName . '.pricing') :
|
||||||
|
config('pricing.test.' . $productName . '.pricing');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getProductId($productName = 'default')
|
||||||
|
{
|
||||||
|
return App::environment() == 'production' ?
|
||||||
|
config('pricing.production.' . $productName . '.product_id') :
|
||||||
|
config('pricing.test.' . $productName . '.product_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getLineItemInterval(SubscriptionItem $item)
|
||||||
|
{
|
||||||
|
return $item->price->recurring->interval === 'year' ? 'yearly' : 'monthly';
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserHelper
|
||||||
|
{
|
||||||
|
public function __construct(public User $user)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get to total number of active members in each of this user's workspaces
|
||||||
|
*/
|
||||||
|
public function getActiveMembersCount(): ?int
|
||||||
|
{
|
||||||
|
$count = 1;
|
||||||
|
foreach ($this->user->workspaces as $workspace) {
|
||||||
|
$count += $workspace->users()->where('users.id', '!=', $this->user->id)->count();
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,13 @@ class WorkspaceHelper
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRecords($relatedRecordIds = null)
|
public function getAllUsers()
|
||||||
{
|
{
|
||||||
return [];
|
return $this->workspace->users()->withPivot('role')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllInvites()
|
||||||
|
{
|
||||||
|
return $this->workspace->invites()->get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,11 @@ if(!function_exists('front_url')) {
|
||||||
return rtrim($baseUrl, '/').'/'.ltrim($path, '/');
|
return rtrim($baseUrl, '/').'/'.ltrim($path, '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(!function_exists('pricing_enabled')) {
|
||||||
|
function pricing_enabled(): bool
|
||||||
|
{
|
||||||
|
return App::environment() !== 'testing' && !is_null(config('cashier.key'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="closeable"
|
v-if="closeable"
|
||||||
class="absolute top-4 right-4"
|
class="absolute top-4 right-4 z-10"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showAuth"
|
v-if="showAuth"
|
||||||
class="hidden md:block ml-auto relative"
|
class="hidden md:flex gap-x-2 ml-auto"
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="$route.name !== 'templates'"
|
v-if="$route.name !== 'templates'"
|
||||||
:to="{ name: 'templates' }"
|
:to="{ name: 'templates' }"
|
||||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
:class="navLinkClasses"
|
||||||
>
|
>
|
||||||
Templates
|
Templates
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<template v-if="featureBaseEnabled">
|
<template v-if="featureBaseEnabled">
|
||||||
<button
|
<button
|
||||||
v-if="user"
|
v-if="user"
|
||||||
class="text-sm text-gray-600 dark:text-white hidden sm:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
:class="navLinkClasses"
|
||||||
@click.prevent="openChangelog"
|
@click.prevent="openChangelog"
|
||||||
>
|
>
|
||||||
What's new? <span
|
What's new? <span
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
v-else
|
v-else
|
||||||
:href="opnformConfig.links.changelog_url"
|
:href="opnformConfig.links.changelog_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
:class="navLinkClasses"
|
||||||
>
|
>
|
||||||
What's new?
|
What's new?
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -56,7 +56,8 @@
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="$route.name !== 'ai-form-builder' && user === null"
|
v-if="$route.name !== 'ai-form-builder' && user === null"
|
||||||
:to="{ name: 'ai-form-builder' }"
|
:to="{ name: 'ai-form-builder' }"
|
||||||
class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
:class="navLinkClasses"
|
||||||
|
class="hidden lg:inline"
|
||||||
>
|
>
|
||||||
AI Form Builder
|
AI Form Builder
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
@ -67,15 +68,18 @@
|
||||||
$route.name !== 'pricing'
|
$route.name !== 'pricing'
|
||||||
"
|
"
|
||||||
:to="{ name: 'pricing' }"
|
:to="{ name: 'pricing' }"
|
||||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
:class="navLinkClasses"
|
||||||
>
|
>
|
||||||
<span v-if="user">Upgrade</span>
|
<span
|
||||||
|
v-if="user"
|
||||||
|
class="text-primary"
|
||||||
|
>Upgrade</span>
|
||||||
<span v-else>Pricing</span>
|
<span v-else>Pricing</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:href="helpUrl"
|
:href="helpUrl"
|
||||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
|
:class="navLinkClasses"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
|
|
@ -90,7 +94,7 @@
|
||||||
class="block"
|
class="block"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="ml-3 mr-4 relative">
|
<div class="ml-4 relative">
|
||||||
<div class="relative inline-block text-left">
|
<div class="relative inline-block text-left">
|
||||||
<dropdown
|
<dropdown
|
||||||
v-if="user"
|
v-if="user"
|
||||||
|
|
@ -100,7 +104,8 @@
|
||||||
<button
|
<button
|
||||||
id="dropdown-menu-button"
|
id="dropdown-menu-button"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center justify-center w-full rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-50 hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-gray-500"
|
:class="navLinkClasses"
|
||||||
|
class="flex items-center"
|
||||||
dusk="nav-dropdown-button"
|
dusk="nav-dropdown-button"
|
||||||
@click.stop="toggle()"
|
@click.stop="toggle()"
|
||||||
>
|
>
|
||||||
|
|
@ -117,7 +122,7 @@
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="userOnboarded"
|
v-if="userOnboarded"
|
||||||
:to="{ name: 'home' }"
|
:to="{ name: 'home' }"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -139,7 +144,7 @@
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="userOnboarded"
|
v-if="userOnboarded"
|
||||||
:to="{ name: 'templates-my-templates' }"
|
:to="{ name: 'templates-my-templates' }"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -160,7 +165,7 @@
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="{ name: 'settings-profile' }"
|
:to="{ name: 'settings-profile' }"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="w-4 h-4 mr-2"
|
class="w-4 h-4 mr-2"
|
||||||
|
|
@ -188,7 +193,7 @@
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="user.moderator"
|
v-if="user.moderator"
|
||||||
:to="{ name: 'settings-admin' }"
|
:to="{ name: 'settings-admin' }"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -209,7 +214,7 @@
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="block px-4 py-2 text-md text-gray-700 hover:no-underline transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
@click.prevent="logout"
|
@click.prevent="logout"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -236,7 +241,7 @@
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="$route.name !== 'login'"
|
v-if="$route.name !== 'login'"
|
||||||
:to="{ name: 'login' }"
|
:to="{ name: 'login' }"
|
||||||
class="text-gray-600 dark:text-white hover:text-gray-800 dark:hover:text-white px-0 sm:px-3 py-2 rounded-md text-sm"
|
:class="navLinkClasses"
|
||||||
active-class="text-gray-800 dark:text-white"
|
active-class="text-gray-800 dark:text-white"
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
|
|
@ -245,6 +250,7 @@
|
||||||
<v-button
|
<v-button
|
||||||
v-track.nav_create_form_click
|
v-track.nav_create_form_click
|
||||||
size="small"
|
size="small"
|
||||||
|
class="shrink-0"
|
||||||
:to="{ name: 'forms-create-guest' }"
|
:to="{ name: 'forms-create-guest' }"
|
||||||
color="outline-blue"
|
color="outline-blue"
|
||||||
:arrow="true"
|
:arrow="true"
|
||||||
|
|
@ -290,6 +296,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
navLinkClasses: 'border border-transparent hover:border-gray-200 text-gray-500 hover:text-gray-800 hover:no-underline dark:hover:text-white py-2 px-3 hover:bg-gray-50 rounded-md text-sm font-medium transition-colors w-full md:w-auto text-center md:text-left'
|
||||||
|
}),
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
helpUrl() {
|
helpUrl() {
|
||||||
return this.opnformConfig.links.help_url
|
return this.opnformConfig.links.help_url
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<dropdown
|
<Dropdown
|
||||||
v-if="user && workspaces && workspaces.length > 1"
|
v-if="user && workspaces && workspaces.length > 1"
|
||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
dropdown-class="origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
dropdown-class="origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
||||||
|
|
@ -10,72 +10,47 @@
|
||||||
#trigger="{ toggle }"
|
#trigger="{ toggle }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center cursor group"
|
class="flex items-center cursor border border-transparent hover:border-gray-200 py-2 px-3 hover:bg-gray-50 rounded-md transition-colors"
|
||||||
role="button"
|
role="button"
|
||||||
@click.stop="toggle()"
|
@click.stop="toggle()"
|
||||||
>
|
>
|
||||||
<div class="rounded-full h-8 8">
|
<WorkspaceIcon :workspace="workspace" />
|
||||||
<img
|
|
||||||
v-if="isUrl(workspace.icon)"
|
|
||||||
:src="workspace.icon"
|
|
||||||
:alt="workspace.name + ' icon'"
|
|
||||||
class="flex-shrink-0 h-8 w-8 rounded-full shadow"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="rounded-full pt-2 text-xs truncate bg-nt-blue-lighter h-8 w-8 text-center shadow"
|
|
||||||
v-text="workspace.icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
<p
|
||||||
class="hidden group-hover:underline lg:block max-w-10 truncate ml-2 text-gray-800 dark:text-gray-200"
|
class="hidden md:block max-w-10 truncate text-sm ml-2 text-gray-800 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{{ workspace.name }}
|
{{ workspace.name }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template
|
<div class="px-1">
|
||||||
|
<a
|
||||||
v-for="worksp in workspaces"
|
v-for="worksp in workspaces"
|
||||||
:key="worksp.id"
|
:key="worksp.id"
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="#"
|
href="#"
|
||||||
class="px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
class="px-4 py-2 text-md rounded text-gray-700 hover:no-underline hover:bg-neutral-50 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-blue-100 dark:bg-blue-900': workspace?.id === worksp?.id,
|
'bg-blue-100 dark:bg-blue-900 hover:bg-blue-200':
|
||||||
|
workspace?.id === worksp?.id,
|
||||||
}"
|
}"
|
||||||
@click.prevent="switchWorkspace(worksp)"
|
@click.prevent="switchWorkspace(worksp)"
|
||||||
>
|
>
|
||||||
<div
|
<WorkspaceIcon :workspace="worksp" />
|
||||||
class="rounded-full h-8 w-8 flex-shrink-0"
|
<p class="ml-4 truncate text-sm">{{ worksp.name }}</p>
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isUrl(worksp.icon)"
|
|
||||||
:src="worksp.icon"
|
|
||||||
:alt="worksp.name + ' icon'"
|
|
||||||
class="flex-shrink-0 h-8 w-8 rounded-full shadow"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="rounded-full flex-shrink-0 pt-1 text-xs truncate bg-nt-blue-lighter h-8 w-8 text-center shadow"
|
|
||||||
v-text="worksp.icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="ml-4 truncate">{{ worksp.name }}</p>
|
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</div>
|
||||||
</dropdown>
|
</Dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import Dropdown from "~/components/global/Dropdown.vue"
|
import Dropdown from "~/components/global/Dropdown.vue"
|
||||||
|
import WorkspaceIcon from "~/components/workspaces/WorkspaceIcon.vue"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "WorkspaceDropdown",
|
name: "WorkspaceDropdown",
|
||||||
components: {
|
components: {
|
||||||
|
WorkspaceIcon,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -114,14 +89,6 @@ export default {
|
||||||
}
|
}
|
||||||
this.formsStore.loadAll(workspace.id)
|
this.formsStore.loadAll(workspace.id)
|
||||||
},
|
},
|
||||||
isUrl(str) {
|
|
||||||
try {
|
|
||||||
new URL(str)
|
|
||||||
} catch (_) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -20,25 +20,13 @@
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<template #title>
|
<template #title>
|
||||||
Change form workspace
|
Change form's workspace
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex space-x-4 items-center">
|
<div class="flex space-x-4 items-center">
|
||||||
<p>Current workspace:</p>
|
<p>Current workspace:</p>
|
||||||
<div class="flex items-center cursor group p-2 rounded border">
|
<div class="flex items-center cursor group p-2 rounded border">
|
||||||
<div class="rounded-full h-8 8">
|
<WorkspaceIcon :workspace="workspace" />
|
||||||
<img
|
|
||||||
v-if="isUrl(workspace.icon)"
|
|
||||||
:src="workspace.icon"
|
|
||||||
:alt="workspace.name + ' icon'"
|
|
||||||
class="flex-shrink-0 h-8 w-8 rounded-full shadow"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="rounded-full pt-2 text-xs truncate bg-nt-blue-lighter h-8 w-8 text-center shadow"
|
|
||||||
v-text="workspace.icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
<p
|
||||||
class="lg:block max-w-10 truncate ml-2 text-gray-800 dark:text-gray-200"
|
class="lg:block max-w-10 truncate ml-2 text-gray-800 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
|
|
@ -54,7 +42,7 @@
|
||||||
class=""
|
class=""
|
||||||
:options="workspacesSelectOptions"
|
:options="workspacesSelectOptions"
|
||||||
:required="true"
|
:required="true"
|
||||||
label="Select workspace"
|
label="Select destination workspace"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-4 pb-5">
|
<div class="flex justify-end mt-4 pb-5">
|
||||||
|
|
@ -78,6 +66,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits, computed } from "vue"
|
import { ref, defineProps, defineEmits, computed } from "vue"
|
||||||
|
import WorkspaceIcon from "~/components/workspaces/WorkspaceIcon.vue"
|
||||||
const emit = defineEmits(["close"])
|
const emit = defineEmits(["close"])
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
const formsStore = useFormsStore()
|
const formsStore = useFormsStore()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
v-if="isWorkspaceAdmin"
|
||||||
|
class="my-2"
|
||||||
|
@submit.prevent="addUser"
|
||||||
|
>
|
||||||
|
<text-input
|
||||||
|
v-model="newUser"
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
:required="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
placeholder="Add a new user by email"
|
||||||
|
/>
|
||||||
|
<select-input
|
||||||
|
v-model="newUserRole"
|
||||||
|
name="newUserRole"
|
||||||
|
:options="roleOptions"
|
||||||
|
:disabled="disabled"
|
||||||
|
placeholder="Select User Role"
|
||||||
|
label="Role"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-center mt-2">
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
:disabled="disabled"
|
||||||
|
:loading="addingUsersState"
|
||||||
|
icon="i-heroicons-envelope"
|
||||||
|
>
|
||||||
|
Invite User
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { watch, ref } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isWorkspaceAdmin: {},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['fetchUsers'])
|
||||||
|
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
|
||||||
|
const roleOptions = [
|
||||||
|
{ name: "User", value: "user" },
|
||||||
|
{ name: "Admin", value: "admin" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const newUser = ref("")
|
||||||
|
const newUserRole = ref("user")
|
||||||
|
const addingUsersState = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
if (!newUser.value) return
|
||||||
|
addingUsersState.value = true
|
||||||
|
opnFetch(
|
||||||
|
"/open/workspaces/" + workspacesStore.currentId + "/users/add",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
email: newUser.value,
|
||||||
|
role: newUserRole.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then((data) => {
|
||||||
|
newUser.value = ""
|
||||||
|
newUserRole.value = "user"
|
||||||
|
|
||||||
|
useAlert().success(data.message)
|
||||||
|
|
||||||
|
emit("fetchUsers")
|
||||||
|
}).catch((error) => {
|
||||||
|
useAlert().error("There was an error adding user")
|
||||||
|
}).finally(() => {
|
||||||
|
addingUsersState.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
:show="showEditUserModal"
|
||||||
|
max-width="lg"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
Edit User Role
|
||||||
|
</template>
|
||||||
|
<div class="px-4">
|
||||||
|
<form
|
||||||
|
@submit.prevent="updateUserRole"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FlatSelectInput
|
||||||
|
v-model="userNewRole"
|
||||||
|
name="newUserRole"
|
||||||
|
:label="'New Role for '+props.user.name"
|
||||||
|
:options="[
|
||||||
|
{ name: 'User', value: 'user' },
|
||||||
|
{ name: 'Admin', value: 'admin' }
|
||||||
|
]"
|
||||||
|
option-key="value"
|
||||||
|
display-key="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-6">
|
||||||
|
<v-button
|
||||||
|
:loading="updatingUserRoleState"
|
||||||
|
class="w-full my-3"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</v-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {watch, ref} from "vue"
|
||||||
|
|
||||||
|
const props = defineProps(['user', 'showEditUserModal'])
|
||||||
|
const emit = defineEmits(['close', 'fetchUsers'])
|
||||||
|
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
const userNewRole = ref("")
|
||||||
|
|
||||||
|
const updatingUserRoleState = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.user, () => {
|
||||||
|
userNewRole.value = props.user.pivot.role
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateUserRole = () => {
|
||||||
|
updatingUserRoleState.value = true
|
||||||
|
opnFetch(
|
||||||
|
"/open/workspaces/" +
|
||||||
|
workspacesStore.currentId +
|
||||||
|
"/users/" +
|
||||||
|
props.user.id +
|
||||||
|
"/update-role",
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
role: userNewRole.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{showSuccess: false},
|
||||||
|
).then(() => {
|
||||||
|
useAlert().success("User role updated.")
|
||||||
|
emit('fetchUsers')
|
||||||
|
emit('close')
|
||||||
|
}).catch((error) => {
|
||||||
|
useAlert().error("There was an error updating user role")
|
||||||
|
}).finally(() => {
|
||||||
|
updatingUserRoleState.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
:form="form"
|
:form="form"
|
||||||
label="Email"
|
label="Email"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
:disabled="disableEmail"
|
||||||
placeholder="Your email address"
|
placeholder="Your email address"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -135,6 +136,7 @@ export default {
|
||||||
agree_terms: false,
|
agree_terms: false,
|
||||||
appsumo_license: null,
|
appsumo_license: null,
|
||||||
}),
|
}),
|
||||||
|
disableEmail:false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -167,12 +169,26 @@ export default {
|
||||||
) {
|
) {
|
||||||
this.form.appsumo_license = this.$route.query.appsumo_license
|
this.form.appsumo_license = this.$route.query.appsumo_license
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.$route.query?.invite_token) {
|
||||||
|
if (this.$route.query?.email) {
|
||||||
|
this.form.email = this.$route.query?.email
|
||||||
|
this.disableEmail = true
|
||||||
|
}
|
||||||
|
this.form.invite_token = this.$route.query?.invite_token
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async register() {
|
async register() {
|
||||||
|
let data
|
||||||
|
try {
|
||||||
// Register the user.
|
// Register the user.
|
||||||
const data = await this.form.post("/register")
|
data = await this.form.post("/register")
|
||||||
|
} catch (err) {
|
||||||
|
useAlert().error(err.response?._data?.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Log in the user.
|
// Log in the user.
|
||||||
const tokenData = await this.form.post("/login")
|
const tokenData = await this.form.post("/login")
|
||||||
|
|
@ -215,9 +231,15 @@ export default {
|
||||||
// Redirect
|
// Redirect
|
||||||
if (this.isQuick) {
|
if (this.isQuick) {
|
||||||
this.$emit("afterQuickLogin")
|
this.$emit("afterQuickLogin")
|
||||||
|
} else {
|
||||||
|
// If is invite just redirect to home
|
||||||
|
if (this.form.invite_token) {
|
||||||
|
useAlert().success("You have successfully accepted the invite and joined this workspace.")
|
||||||
|
this.$router.push({name: "home"})
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({name: "forms-create"})
|
this.$router.push({name: "forms-create"})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="border relative max-w-5xl mx-auto mt-4 lg:mt-10">
|
<div class="border lg:rounded-xl bg-gray-50 dark:bg-gray-800 relative max-w-5xl mx-auto mt-10">
|
||||||
<div class="w-full">
|
|
||||||
<div
|
<div
|
||||||
class="rounded-lg bg-gray-50 dark:bg-gray-800 px-6 py-8 sm:p-10 lg:flex lg:items-center"
|
class=" p-6 lg:flex lg:items-center"
|
||||||
>
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3
|
<h3
|
||||||
class="inline-flex px-4 py-1 rounded-full text-md font-bold tracking-wide uppercase bg-white text-gray-800"
|
class="inline-flex px-4 py-1 rounded-full text-md font-semibold tracking-wide bg-blue-500 text-white"
|
||||||
>
|
>
|
||||||
Custom plan
|
Custom Plan
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-4 text-md text-gray-600 dark:text-gray-400">
|
<div class="mt-4 text-gray-600 dark:text-gray-400 max-w-2xl">
|
||||||
Get a custom file upload limit, enterprise-level support, custom
|
Get a custom file upload limit, enterprise-level support, custom
|
||||||
contract, dedicated application instance in a specific region,
|
contract, dedicated application instance in a specific region,
|
||||||
payment via invoice/PO etc.
|
payment via invoice/PO etc.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 rounded-md lg:mt-0 lg:ml-10 lg:flex-shrink-0">
|
<div class="mt-6 lg:mt-0 lg:ml-10 lg:flex-shrink-0">
|
||||||
<v-button
|
<UButton
|
||||||
|
size="xl"
|
||||||
color="white"
|
color="white"
|
||||||
class="w-full mt-4"
|
class="w-auto"
|
||||||
|
icon="i-heroicons-chat-bubble-left"
|
||||||
@click.prevent="customPlanClick"
|
@click.prevent="customPlanClick"
|
||||||
>
|
>
|
||||||
Contact us
|
Contact us
|
||||||
</v-button>
|
</UButton>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,10 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</li>
|
</li>
|
||||||
<slot name="pricing-table" />
|
<slot
|
||||||
|
name="pricing-table"
|
||||||
|
:is-yearly="isYearly"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="customDomainsEnabled"
|
||||||
|
id="custom-domains"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
label="Manage Custom Domains"
|
||||||
|
icon="i-heroicons-globe-alt"
|
||||||
|
@click="showCustomDomainModal = !showCustomDomainModal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<modal
|
||||||
|
:show="showCustomDomainModal"
|
||||||
|
max-width="lg"
|
||||||
|
@close="showCustomDomainModal = false"
|
||||||
|
>
|
||||||
|
<h4 class="mb-4 font-medium">
|
||||||
|
Manage your custom domains
|
||||||
|
</h4>
|
||||||
|
<UAlert
|
||||||
|
v-if="!workspace.is_pro"
|
||||||
|
icon="i-heroicons-user-group-20-solid"
|
||||||
|
class="mb-4"
|
||||||
|
color="orange"
|
||||||
|
variant="subtle"
|
||||||
|
title="Pro plan required"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
Please <NuxtLink
|
||||||
|
:to="{name:'pricing'}"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
upgrade your account
|
||||||
|
</NuxtLink> to setup a custom domain.
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
<p class="text-gray-500 text-sm mb-4">
|
||||||
|
Read
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="underline"
|
||||||
|
@click.prevent="
|
||||||
|
crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')
|
||||||
|
"
|
||||||
|
>our instructions</a>
|
||||||
|
to learn how to setup your own domain.
|
||||||
|
</p>
|
||||||
|
<text-area-input
|
||||||
|
:form="customDomainsForm"
|
||||||
|
name="custom_domains"
|
||||||
|
:required="false"
|
||||||
|
:disabled="!workspace.is_pro"
|
||||||
|
label="Workspace Custom Domains"
|
||||||
|
wrapper-class=""
|
||||||
|
placeholder="yourdomain.com - 1 per line"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
class="mt-3"
|
||||||
|
:loading="customDomainsLoading"
|
||||||
|
:disabled="!workspace.is_pro"
|
||||||
|
icon="i-heroicons-check"
|
||||||
|
@click="saveChanges"
|
||||||
|
>
|
||||||
|
Save Domain(s)
|
||||||
|
</UButton>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {watch} from "vue"
|
||||||
|
|
||||||
|
const crisp = useCrisp()
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
const loading = computed(() => workspacesStore.loading)
|
||||||
|
|
||||||
|
const customDomainsForm = useForm({
|
||||||
|
custom_domain: "",
|
||||||
|
})
|
||||||
|
const customDomainsLoading = ref(false)
|
||||||
|
const showCustomDomainModal = ref(false)
|
||||||
|
|
||||||
|
const customDomainsEnabled = computed(
|
||||||
|
() => useRuntimeConfig().public.customDomainsEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCustomDomains()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => workspace,
|
||||||
|
() => {
|
||||||
|
initCustomDomains()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
if (customDomainsLoading.value) return
|
||||||
|
customDomainsLoading.value = true
|
||||||
|
|
||||||
|
// Update the workspace custom domain
|
||||||
|
customDomainsForm
|
||||||
|
.put("/open/workspaces/" + workspace.value.id + "/custom-domains", {
|
||||||
|
data: {
|
||||||
|
custom_domains: customDomainsForm?.custom_domains
|
||||||
|
?.split("\n")
|
||||||
|
.map((domain) => (domain ? domain.trim() : null))
|
||||||
|
.filter((domain) => domain && domain.length > 0),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
workspacesStore.save(data)
|
||||||
|
useAlert().success("Custom domains saved.")
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
useAlert().error(
|
||||||
|
"Failed to update custom domains: " + error.response.data.message,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
customDomainsLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initCustomDomains = () => {
|
||||||
|
if (!workspace || !workspace.value.custom_domains) return
|
||||||
|
customDomainsForm.custom_domains = workspace.value?.custom_domains.join("\n")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,362 @@
|
||||||
|
<template>
|
||||||
|
<div class="border rounded-md p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h4 class="font-semibold">
|
||||||
|
Workspace Members
|
||||||
|
</h4>
|
||||||
|
<UButton
|
||||||
|
label="Invite User"
|
||||||
|
icon="i-heroicons-user-plus-20-solid"
|
||||||
|
:loading="loading"
|
||||||
|
@click="userInviteModal = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- User invite modal -->
|
||||||
|
<modal
|
||||||
|
:show="userInviteModal"
|
||||||
|
max-width="lg"
|
||||||
|
@close="userInviteModal = false"
|
||||||
|
>
|
||||||
|
<h4 class="mb-4 font-medium">
|
||||||
|
Invite a new user and collaborate on building forms
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<template v-if="paidPlansEnabled">
|
||||||
|
<UAlert
|
||||||
|
v-if="workspace.is_pro"
|
||||||
|
icon="i-heroicons-credit-card"
|
||||||
|
color="primary"
|
||||||
|
variant="subtle"
|
||||||
|
title="This is a billable event."
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
You will be charged $6/month for each user you invite to this workspace. More details on the
|
||||||
|
<NuxtLink
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
:to="{name:'settings-billing'}"
|
||||||
|
>
|
||||||
|
billing
|
||||||
|
</NuxtLink>
|
||||||
|
and
|
||||||
|
<NuxtLink
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
:to="{name:'pricing'}"
|
||||||
|
>
|
||||||
|
pricing
|
||||||
|
</NuxtLink>
|
||||||
|
page.
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
<UAlert
|
||||||
|
v-else
|
||||||
|
icon="i-heroicons-user-group-20-solid"
|
||||||
|
color="orange"
|
||||||
|
variant="subtle"
|
||||||
|
title="Pro plan required"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
You need a Pro plan to invite new users on OpnForm. Please upgrade on our
|
||||||
|
<NuxtLink
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
:to="{name:'pricing'}"
|
||||||
|
>
|
||||||
|
pricing
|
||||||
|
</NuxtLink>
|
||||||
|
page.
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<AddUserToWorkspace
|
||||||
|
:disabled="!canInviteUser"
|
||||||
|
:is-workspace-admin="isWorkspaceAdmin"
|
||||||
|
@fetch-users="getWorkspaceUsers"
|
||||||
|
/>
|
||||||
|
</modal>
|
||||||
|
<UTable
|
||||||
|
class="-mx-4 border-y mt-4"
|
||||||
|
:loading="loadingUsers"
|
||||||
|
:rows="users"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="isWorkspaceAdmin"
|
||||||
|
#actions-data="{ row, index }"
|
||||||
|
>
|
||||||
|
<div class="space-x-2 flex justify-center">
|
||||||
|
<template v-if="row.type == 'user'">
|
||||||
|
<p
|
||||||
|
v-if="row.is_current_user"
|
||||||
|
class="text-gray-500 text-center text-sm"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</p>
|
||||||
|
<UButtonGroup
|
||||||
|
v-else
|
||||||
|
size="2xs"
|
||||||
|
>
|
||||||
|
<UTooltip
|
||||||
|
text="Edit user"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-pencil"
|
||||||
|
color="gray"
|
||||||
|
class="hover:text-blue-500"
|
||||||
|
square
|
||||||
|
@click="editUser(index)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
text="Remove user"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
v-if="row.type == 'user'"
|
||||||
|
icon="i-heroicons-trash"
|
||||||
|
color="gray"
|
||||||
|
class="hover:text-red-500"
|
||||||
|
square
|
||||||
|
@click="removeUser(index)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</UButtonGroup>
|
||||||
|
</template>
|
||||||
|
<UButtonGroup
|
||||||
|
v-else-if="row.type == 'invitee'"
|
||||||
|
size="2xs"
|
||||||
|
>
|
||||||
|
<UTooltip
|
||||||
|
text="Resend Invite"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-envelope"
|
||||||
|
color="gray"
|
||||||
|
class="hover:text-blue-500"
|
||||||
|
square
|
||||||
|
@click="resendInvite(index)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
text="Cancel Invite"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-trash"
|
||||||
|
color="gray"
|
||||||
|
class="hover:text-red-500"
|
||||||
|
square
|
||||||
|
@click="cancelInvite(index)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</UButtonGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
|
||||||
|
<EditWorkSpaceUser
|
||||||
|
:user="selectedUser"
|
||||||
|
:show-edit-user-modal="showEditUserModal"
|
||||||
|
@close="showEditUserModal = false"
|
||||||
|
@fetch-users="getWorkspaceUsers"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<UButton
|
||||||
|
v-if="users.length > 1"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-arrow-left-start-on-rectangle-20-solid"
|
||||||
|
:loading="leaveWorkspaceLoadingState"
|
||||||
|
@click="leaveWorkSpace(workspace.id)"
|
||||||
|
>
|
||||||
|
Leave Workspace
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="isWorkspaceAdmin && users.length == 1"
|
||||||
|
icon="i-heroicons-trash"
|
||||||
|
color="gray"
|
||||||
|
:loading="loading"
|
||||||
|
@click="deleteWorkspace(workspace.id)"
|
||||||
|
>
|
||||||
|
Remove workspace
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
const loading = computed(() => workspacesStore.loading)
|
||||||
|
const workspaces = computed(() => workspacesStore.getAll)
|
||||||
|
|
||||||
|
const users = ref([])
|
||||||
|
const loadingUsers = ref(true)
|
||||||
|
const leaveWorkspaceLoadingState = ref(false)
|
||||||
|
|
||||||
|
const userInviteModal = ref(false)
|
||||||
|
const showEditUserModal = ref(false)
|
||||||
|
const selectedUser = ref(null)
|
||||||
|
const userNewRole = ref("")
|
||||||
|
|
||||||
|
const paidPlansEnabled = computed(() => useRuntimeConfig().public.paidPlansEnabled)
|
||||||
|
const canInviteUser = computed(() => {
|
||||||
|
return paidPlansEnabled.value ? workspace.value.is_pro : true
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkspaceUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getWorkspaceUsers = async () => {
|
||||||
|
userInviteModal.value = false
|
||||||
|
loadingUsers.value = true
|
||||||
|
let data = await workspacesStore.getWorkspaceUsers()
|
||||||
|
data = data.map(d => {
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
id: d.id,
|
||||||
|
is_current_user: d.id === authStore.user.id,
|
||||||
|
name: d.name,
|
||||||
|
email: d.email,
|
||||||
|
status: 'accepted',
|
||||||
|
role: d.pivot.role,
|
||||||
|
type: 'user'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let invites = await workspacesStore.getWorkspaceInvites()
|
||||||
|
invites = invites.filter(i => i.status !== 'accepted').map(i => {
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
name: 'Invitee',
|
||||||
|
email: i.email,
|
||||||
|
status: i.status,
|
||||||
|
type: 'invitee'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
users.value = [...data, ...invites]
|
||||||
|
loadingUsers.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWorkspaceAdmin = computed(() => {
|
||||||
|
if (!users.value) return false
|
||||||
|
const user = users.value.find((user) => user.id === authStore.user.id)
|
||||||
|
return user && user.pivot.role === "admin"
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{key: 'name', label: 'Name'},
|
||||||
|
{key: 'email', label: 'Email'},
|
||||||
|
{key: 'role', label: 'Role'},
|
||||||
|
...(isWorkspaceAdmin.value ? [{key: 'actions', label: 'Action', class: 'text-center'}] : [])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const editUser = (row) => {
|
||||||
|
selectedUser.value = users.value[row]
|
||||||
|
userNewRole.value = selectedUser.value.pivot.role
|
||||||
|
showEditUserModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const removeUser = (index) => {
|
||||||
|
const user = users.value[index]
|
||||||
|
useAlert().confirm(
|
||||||
|
"Do you really want to remove " + user.name + " from this workspace?",
|
||||||
|
() => {
|
||||||
|
loadingUsers.value = true
|
||||||
|
opnFetch(
|
||||||
|
"/open/workspaces/" + workspacesStore.currentId + "/users/" + user.id + "/remove",
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
{showSuccess: false},
|
||||||
|
).then(() => {
|
||||||
|
useAlert().success("User successfully removed.")
|
||||||
|
getWorkspaceUsers()
|
||||||
|
}).catch((error) => {
|
||||||
|
useAlert().error("There was an error removing user")
|
||||||
|
}).finally(() => {
|
||||||
|
loadingUsers.value = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteWorkspace = (workspaceId) => {
|
||||||
|
if (workspaces.length <= 1) {
|
||||||
|
useAlert().error("You cannot delete your only workspace.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
useAlert().confirm(
|
||||||
|
"Do you really want to delete this workspace? All forms created in this workspace will be removed.",
|
||||||
|
() => {
|
||||||
|
opnFetch("/open/workspaces/" + workspaceId, {method: "DELETE"}).then(
|
||||||
|
() => {
|
||||||
|
useAlert().success("Workspace successfully removed.")
|
||||||
|
workspacesStore.remove(workspaceId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaveWorkSpace = (workspaceId) => {
|
||||||
|
useAlert().confirm(
|
||||||
|
"Do you really want to leave this workspace? You will lose access to all forms in this workspace.",
|
||||||
|
() => {
|
||||||
|
leaveWorkspaceLoadingState.value = true
|
||||||
|
opnFetch("/open/workspaces/" + workspaceId + "/leave", {
|
||||||
|
method: "POST",
|
||||||
|
}).then(() => {
|
||||||
|
useAlert().success("You have left the workspace.")
|
||||||
|
workspacesStore.remove(workspaceId)
|
||||||
|
getWorkspaceUsers()
|
||||||
|
}).catch((error) => {
|
||||||
|
useAlert().error("There was an error leaving the workspace.")
|
||||||
|
}).finally(() => {
|
||||||
|
leaveWorkspaceLoadingState.value = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendInvite = (id) => {
|
||||||
|
const inviteId = users.value[id].id
|
||||||
|
useAlert().confirm(
|
||||||
|
"Do you really want to resend invite email to this user?",
|
||||||
|
() => {
|
||||||
|
opnFetch("/open/workspaces/" + workspace.value.id + "/invites/" + inviteId + "/resend", {method: "POST"}).then(
|
||||||
|
() => {
|
||||||
|
useAlert().success("Invitation resent successfully.")
|
||||||
|
getWorkspaceUsers()
|
||||||
|
},
|
||||||
|
).catch(err => {
|
||||||
|
useAlert().error(err.response._data?.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelInvite = (id) => {
|
||||||
|
const inviteId = users.value[id].id
|
||||||
|
useAlert().confirm(
|
||||||
|
"Do you really want to cancel this user's invitation to this workspace?",
|
||||||
|
() => {
|
||||||
|
opnFetch("/open/workspaces/" + workspace.value.id + "/invites/" + inviteId + "/cancel", {method: "DELETE"}).then(
|
||||||
|
() => {
|
||||||
|
useAlert().success("Invitation cancelled successfully.")
|
||||||
|
getWorkspaceUsers()
|
||||||
|
},
|
||||||
|
).catch(err => {
|
||||||
|
useAlert().error(err.response._data?.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -34,6 +34,10 @@
|
||||||
File Size Uploads:
|
File Size Uploads:
|
||||||
<span class="font-semibold">{{ tierFeatures.file_upload_size }}</span>
|
<span class="font-semibold">{{ tierFeatures.file_upload_size }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
Users limit:
|
||||||
|
<span class="font-semibold">{{ tierFeatures.users }}</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="w-max">
|
<div class="w-max">
|
||||||
<v-button
|
<v-button
|
||||||
|
|
@ -43,7 +47,7 @@
|
||||||
href="https://appsumo.com/account/products/"
|
href="https://appsumo.com/account/products/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Mangage in AppSumo
|
Manage in AppSumo
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,16 +84,19 @@ export default {
|
||||||
form_quantity: "Unlimited",
|
form_quantity: "Unlimited",
|
||||||
file_upload_size: "25mb",
|
file_upload_size: "25mb",
|
||||||
domain_names: "5",
|
domain_names: "5",
|
||||||
|
users: 1
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
form_quantity: "Unlimited",
|
form_quantity: "Unlimited",
|
||||||
file_upload_size: "50mb",
|
file_upload_size: "50mb",
|
||||||
domain_names: "25",
|
domain_names: "25",
|
||||||
|
users: 5
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
form_quantity: "Unlimited",
|
form_quantity: "Unlimited",
|
||||||
file_upload_size: "75mb",
|
file_upload_size: "75mb",
|
||||||
domain_names: "Unlimited",
|
domain_names: "Unlimited",
|
||||||
|
users: 'Unlimited'
|
||||||
},
|
},
|
||||||
}[this.licenseTier]
|
}[this.licenseTier]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<img
|
||||||
|
v-if="isUrl(workspace.icon)"
|
||||||
|
:src="workspace.icon"
|
||||||
|
:alt="`${workspace.name} icon`"
|
||||||
|
class="flex-shrink-0 rounded"
|
||||||
|
:class="size"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="size"
|
||||||
|
class="rounded text-xs truncate bg-neutral-100 text-center flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="font-semibold text-neutral-500"
|
||||||
|
v-text="workspace.icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WorkspaceIcon',
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
workspace: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'h-6 w-6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isUrl(str) {
|
||||||
|
try {
|
||||||
|
new URL(str)
|
||||||
|
}
|
||||||
|
catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -6,7 +6,7 @@ import gtm from "./gtm"
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
loglevel: process.env.NUXT_LOG_LEVEL || 'info',
|
loglevel: process.env.NUXT_LOG_LEVEL || 'info',
|
||||||
devtools: {enabled: false},
|
devtools: {enabled: true},
|
||||||
css: ['~/scss/app.scss'],
|
css: ['~/scss/app.scss'],
|
||||||
modules: [
|
modules: [
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
|
|
@ -70,7 +70,7 @@ export default defineNuxtConfig({
|
||||||
classPrefix: '',
|
classPrefix: '',
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
icons: ['heroicons','material-symbols'],
|
icons: ['heroicons', 'material-symbols'],
|
||||||
},
|
},
|
||||||
sitemap,
|
sitemap,
|
||||||
runtimeConfig,
|
runtimeConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div
|
||||||
|
id="public-form"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
|
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
|
||||||
<div v-if="form.cover_picture">
|
<div v-if="form.cover_picture">
|
||||||
<div
|
<div
|
||||||
|
|
@ -235,3 +238,19 @@ useHead({
|
||||||
script: [{ src: '/widgets/iframeResizer.contentWindow.min.js' } ]
|
script: [{ src: '/widgets/iframeResizer.contentWindow.min.js' } ]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#public-form {
|
||||||
|
p, div {
|
||||||
|
@apply text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
@apply text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-blue-600 hover:underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex bg-white">
|
<div class="flex bg-white">
|
||||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl">
|
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl">
|
||||||
<div class="mt-8 pb-0">
|
<div class="mt-4 pb-0">
|
||||||
<text-input
|
<text-input
|
||||||
v-if="forms.length > 0"
|
v-if="forms.length > 0"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
|
|
@ -180,6 +180,40 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!workspace.is_pro"
|
||||||
|
class="px-4"
|
||||||
|
>
|
||||||
|
<UAlert
|
||||||
|
class="mt-4"
|
||||||
|
icon="i-heroicons-command-line"
|
||||||
|
color="primary"
|
||||||
|
variant="subtle"
|
||||||
|
description="You can add components to your app using the cli."
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h2 class="font-medium text-lg -mt-2">
|
||||||
|
Discover our Pro plan
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div class="flex flex-wrap sm:flex-nowrap gap-2 items-start">
|
||||||
|
<p class="flex-grow">
|
||||||
|
Remove NoteForms branding, customize forms further, use your custom domain, integrate with your
|
||||||
|
favorite tools, invite users, and more!
|
||||||
|
</p>
|
||||||
|
<UButton
|
||||||
|
v-track.upgrade_banner_home_click
|
||||||
|
:to="{name:'pricing'}"
|
||||||
|
color="white"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
Upgrade Now
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="formsLoading"
|
v-if="formsLoading"
|
||||||
|
|
@ -195,12 +229,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useFormsStore } from "../stores/forms"
|
import {useFormsStore} from "../stores/forms"
|
||||||
import { useWorkspacesStore } from "../stores/workspaces"
|
import {useWorkspacesStore} from "../stores/workspaces"
|
||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
import TextInput from "../components/forms/TextInput.vue"
|
import TextInput from "../components/forms/TextInput.vue"
|
||||||
import ExtraMenu from "../components/pages/forms/show/ExtraMenu.vue"
|
import ExtraMenu from "../components/pages/forms/show/ExtraMenu.vue"
|
||||||
import { refDebounced } from "@vueuse/core"
|
import {refDebounced} from "@vueuse/core"
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
|
|
@ -216,6 +250,8 @@ const formsStore = useFormsStore()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
formsStore.startLoading()
|
formsStore.startLoading()
|
||||||
|
|
||||||
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!formsStore.allLoaded) {
|
if (!formsStore.allLoaded) {
|
||||||
formsStore.loadAll(workspacesStore.currentId)
|
formsStore.loadAll(workspacesStore.currentId)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,19 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<pricing-table />
|
<pricing-table>
|
||||||
|
<template #pricing-table="{isYearly}">
|
||||||
|
<div class="flex gap-x-2 items-center">
|
||||||
|
<Icon
|
||||||
|
class="inline w-5 h-5 text-blue-500"
|
||||||
|
name="heroicons:user-plus-16-solid"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Extra users for {{ isYearly?'$5/month':'$6/month' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</pricing-table>
|
||||||
|
|
||||||
<section class="py-12 bg-white sm:py-16 lg:py-24 xl:py-24">
|
<section class="py-12 bg-white sm:py-16 lg:py-24 xl:py-24">
|
||||||
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
||||||
|
|
@ -387,12 +399,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { computed } from "vue"
|
import {computed} from "vue"
|
||||||
import { useAuthStore } from "../stores/auth"
|
import {useAuthStore} from "../stores/auth"
|
||||||
import PricingTable from "../components/pages/pricing/PricingTable.vue"
|
import PricingTable from "../components/pages/pricing/PricingTable.vue"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PricingTable },
|
components: {PricingTable},
|
||||||
layout: "default",
|
layout: "default",
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
@ -408,7 +420,7 @@ export default {
|
||||||
// Custom inline middleware
|
// Custom inline middleware
|
||||||
if (!useRuntimeConfig().public.paidPlansEnabled) {
|
if (!useRuntimeConfig().public.paidPlansEnabled) {
|
||||||
// If no paid plan so no need this page
|
// If no paid plan so no need this page
|
||||||
return navigateTo("/", { redirectCode: 301 })
|
return navigateTo("/", {redirectCode: 301})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,15 @@
|
||||||
Create an account
|
Create an account
|
||||||
</h2>
|
</h2>
|
||||||
<small>Sign up in less than 2 minutes.</small>
|
<small>Sign up in less than 2 minutes.</small>
|
||||||
|
<template v-if="!isSelfHosted || isInvited">
|
||||||
<register-form />
|
<register-form />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="my-6 p-3 rounded-lg border border-yellow-600 bg-yellow-200 text-yellow-600"
|
||||||
|
>
|
||||||
|
Registration is not allowed in self host mode.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0">
|
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0">
|
||||||
|
|
@ -108,7 +116,15 @@ export default {
|
||||||
|
|
||||||
data: () => ({}),
|
data: () => ({}),
|
||||||
|
|
||||||
computed: {},
|
computed: {
|
||||||
|
isSelfHosted(){
|
||||||
|
return useRuntimeConfig().public.selfHosted
|
||||||
|
},
|
||||||
|
|
||||||
|
isInvited(){
|
||||||
|
return this.$route.query?.email && this.$route.query?.invite_token
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {},
|
methods: {},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-white">
|
<div class="bg-white">
|
||||||
<div class="flex bg-gray-50">
|
<div class="flex bg-gray-50">
|
||||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
|
<div class="w-full md:w-4/5 md:mx-auto md:max-w-4xl px-4">
|
||||||
<div class="pt-4 pb-0">
|
<div class="pt-4 pb-0">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<h2 class="flex-grow text-gray-900">
|
<h2 class="flex-grow text-gray-900">
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<li>{{ user.email }}</li>
|
<li>{{ user.email }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
|
<div class="mt-4 border-gray-200 dark:border-gray-700">
|
||||||
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
|
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
|
||||||
<li
|
<li
|
||||||
v-for="(tab, i) in tabsList"
|
v-for="(tab, i) in tabsList"
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex bg-white">
|
<div class="flex bg-white">
|
||||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
|
<div class="w-full md:w-4/5 md:mx-auto md:max-w-4xl px-4">
|
||||||
<div class="mt-8 pb-0">
|
<div class="mt-4 pb-0">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,21 @@
|
||||||
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it
|
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it
|
||||||
at any time.</small>
|
at any time.</small>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4 flex flex-wrap gap-2 w-full border shadow rounded-lg p-4 items-center">
|
||||||
<v-button
|
<p
|
||||||
|
v-if="usersCount"
|
||||||
|
class="text-gray-500 flex-grow"
|
||||||
|
>
|
||||||
|
You currently have <span class="font-medium">{{ usersCount }} users</span> in your different workspaces.
|
||||||
|
</p>
|
||||||
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
shade="light"
|
icon="i-heroicons-credit-card"
|
||||||
:loading="billingLoading"
|
:loading="billingLoading"
|
||||||
@click.prevent="openBillingDashboard"
|
@click="openBillingDashboard"
|
||||||
>
|
>
|
||||||
Manage Subscription
|
Manage Subscription
|
||||||
</v-button>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -38,10 +44,25 @@ definePageMeta({
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
let billingLoading = false
|
const billingLoading = ref(false)
|
||||||
|
const usersCount = ref(0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadUsersCount()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadUsersCount = () => {
|
||||||
|
opnFetch("/subscription/users-count")
|
||||||
|
.then((data) => {
|
||||||
|
usersCount.value = data.count
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
useAlert().error(error.data.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const openBillingDashboard = () => {
|
const openBillingDashboard = () => {
|
||||||
billingLoading = true
|
billingLoading.value = true
|
||||||
opnFetch("/subscription/billing-portal")
|
opnFetch("/subscription/billing-portal")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const url = data.portal_url
|
const url = data.portal_url
|
||||||
|
|
@ -51,7 +72,7 @@ const openBillingDashboard = () => {
|
||||||
useAlert().error(error.data.message)
|
useAlert().error(error.data.message)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
billingLoading = false
|
billingLoading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,12 @@
|
||||||
</h3>
|
</h3>
|
||||||
<small class="text-gray-600">Manage your external connections.</small>
|
<small class="text-gray-600">Manage your external connections.</small>
|
||||||
</div>
|
</div>
|
||||||
<v-button
|
<UButton
|
||||||
color="outline-blue"
|
label="Connect new account"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="providerModal = true"
|
@click="providerModal = true"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="inline -mt-1 mr-1 h-4 w-4"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.67"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
Connect new account
|
|
||||||
</v-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap items-center gap-y-4 flex-wrap-reverse">
|
<div class="flex flex-wrap items-center gap-y-4">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<h3 class="font-semibold text-2xl text-gray-900">
|
<h3 class="font-semibold text-2xl text-gray-900">
|
||||||
Workspace settings
|
Workspace settings
|
||||||
</h3>
|
</h3>
|
||||||
<small class="text-gray-600">Manage your workspaces.</small>
|
<small class="text-gray-500">You're currently editing the settings for the workspace "{{ workspace.name }}".
|
||||||
|
You can switch to another workspace in top left corner of the page.</small>
|
||||||
</div>
|
</div>
|
||||||
<v-button
|
<div class="w-full flex flex-wrap justify-between gap-2">
|
||||||
color="outline-blue"
|
<WorkSpaceCustomDomains v-if="customDomainsEnabled && !loading" />
|
||||||
|
<UButton
|
||||||
|
label="New Workspace"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="workspaceModal = true"
|
@click="workspaceModal = true"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="inline -mt-1 mr-1 h-4 w-4"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.67"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</div>
|
||||||
Create new workspace
|
|
||||||
</v-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -36,98 +25,11 @@
|
||||||
>
|
>
|
||||||
<Loader class="h-10 w-10 p-5" />
|
<Loader class="h-10 w-10 p-5" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="workspace">
|
|
||||||
<div class="mt-4 flex group bg-white items-center">
|
|
||||||
<div class="flex space-x-4 flex-grow items-center">
|
|
||||||
<img
|
|
||||||
v-if="isUrl(workspace.icon)"
|
|
||||||
:src="workspace.icon"
|
|
||||||
:alt="workspace.name + ' icon'"
|
|
||||||
class="rounded-full h-12 w-12"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="workspace"
|
||||||
class="rounded-2xl bg-gray-100 h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
|
class="my-4"
|
||||||
v-text="workspace.icon"
|
|
||||||
/>
|
|
||||||
<div class="space-y-4 py-1">
|
|
||||||
<div class="font-bold truncate">
|
|
||||||
{{ workspace.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="customDomainsEnabled">
|
|
||||||
<text-area-input
|
|
||||||
:form="customDomainsForm"
|
|
||||||
name="custom_domains"
|
|
||||||
class="mt-4"
|
|
||||||
:required="false"
|
|
||||||
:disabled="!workspace.is_pro"
|
|
||||||
label="Workspace Custom Domains"
|
|
||||||
wrapper-class=""
|
|
||||||
placeholder="yourdomain.com - 1 per line"
|
|
||||||
/>
|
|
||||||
<p class="text-gray-500 text-sm">
|
|
||||||
Read our
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
@click.prevent="
|
|
||||||
crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')
|
|
||||||
"
|
|
||||||
>custom domain instructions</a>
|
|
||||||
to learn how to use your own domain.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-between gap-2 mt-4">
|
|
||||||
<v-button
|
|
||||||
v-if="customDomainsEnabled"
|
|
||||||
class="w-full sm:w-auto"
|
|
||||||
:loading="customDomainsLoading"
|
|
||||||
@click="saveChanges"
|
|
||||||
>
|
>
|
||||||
<svg
|
<WorkSpaceUser />
|
||||||
class="w-4 h-4 text-white inline mr-1 -mt-1"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Save Domains
|
|
||||||
</v-button>
|
|
||||||
<v-button
|
|
||||||
v-if="workspaces.length > 1"
|
|
||||||
color="white"
|
|
||||||
class="group w-full sm:w-auto"
|
|
||||||
:loading="loading"
|
|
||||||
@click="deleteWorkspace(workspace.id)"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5 -mt-1 inline group-hover:text-red-700"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Remove workspace
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Workspace modal -->
|
<!-- Workspace modal -->
|
||||||
|
|
@ -192,8 +94,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { watch } from "vue"
|
import {watch, ref} from "vue"
|
||||||
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
|
import {fetchAllWorkspaces} from "~/stores/workspaces.js"
|
||||||
|
|
||||||
const crisp = useCrisp()
|
const crisp = useCrisp()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
|
@ -212,91 +114,16 @@ const form = useForm({
|
||||||
emoji: "",
|
emoji: "",
|
||||||
})
|
})
|
||||||
const workspaceModal = ref(false)
|
const workspaceModal = ref(false)
|
||||||
const customDomainsForm = useForm({
|
|
||||||
custom_domain: "",
|
|
||||||
})
|
|
||||||
const customDomainsLoading = ref(false)
|
|
||||||
|
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
const customDomainsEnabled = computed(
|
const customDomainsEnabled = computed(
|
||||||
() => useRuntimeConfig().public.customDomainsEnabled,
|
() => useRuntimeConfig().public.customDomainsEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => workspace,
|
|
||||||
() => {
|
|
||||||
initCustomDomains()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAllWorkspaces()
|
fetchAllWorkspaces()
|
||||||
initCustomDomains()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveChanges = () => {
|
|
||||||
if (customDomainsLoading.value) return
|
|
||||||
customDomainsLoading.value = true
|
|
||||||
|
|
||||||
// Update the workspace custom domain
|
|
||||||
customDomainsForm
|
|
||||||
.put("/open/workspaces/" + workspace.value.id + "/custom-domains", {
|
|
||||||
data: {
|
|
||||||
custom_domains: customDomainsForm?.custom_domains
|
|
||||||
?.split("\n")
|
|
||||||
.map((domain) => (domain ? domain.trim() : null))
|
|
||||||
.filter((domain) => domain && domain.length > 0),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
workspacesStore.save(data)
|
|
||||||
useAlert().success("Custom domains saved.")
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
useAlert().error(
|
|
||||||
"Failed to update custom domains: " + error.response.data.message,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
customDomainsLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const initCustomDomains = () => {
|
|
||||||
if (!workspace || !workspace.value.custom_domains) return
|
|
||||||
customDomainsForm.custom_domains = workspace.value?.custom_domains.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteWorkspace = (workspaceId) => {
|
|
||||||
if (workspaces.length <= 1) {
|
|
||||||
useAlert().error("You cannot delete your only workspace.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
useAlert().confirm(
|
|
||||||
"Do you really want to delete this workspace? All forms created in this workspace will be removed.",
|
|
||||||
() => {
|
|
||||||
opnFetch("/open/workspaces/" + workspaceId, { method: "DELETE" }).then(
|
|
||||||
() => {
|
|
||||||
useAlert().success("Workspace successfully removed.")
|
|
||||||
workspacesStore.remove(workspaceId)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUrl = (str) => {
|
|
||||||
const pattern = new RegExp(
|
|
||||||
"^(https?:\\/\\/)?" + // protocol
|
|
||||||
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
|
|
||||||
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
|
|
||||||
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
|
|
||||||
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
|
|
||||||
"(\\#[-a-z\\d_]*)?$",
|
|
||||||
"i",
|
|
||||||
) // fragment locator
|
|
||||||
return !!pattern.test(str)
|
|
||||||
}
|
|
||||||
const createWorkspace = () => {
|
const createWorkspace = () => {
|
||||||
form.post("/open/workspaces/create").then((data) => {
|
form.post("/open/workspaces/create").then((data) => {
|
||||||
workspacesStore.save(data.workspace)
|
workspacesStore.save(data.workspace)
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,10 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const confetti = useConfetti()
|
||||||
return {
|
return {
|
||||||
authStore,
|
authStore,
|
||||||
|
confetti,
|
||||||
authenticated: computed(() => authStore.check),
|
authenticated: computed(() => authStore.check),
|
||||||
user: computed(() => authStore.user),
|
user: computed(() => authStore.user),
|
||||||
crisp: useCrisp(),
|
crisp: useCrisp(),
|
||||||
|
|
@ -53,6 +55,12 @@ export default {
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
clearInterval(this.interval)
|
clearInterval(this.interval)
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
// stop confettis after 2 sec
|
||||||
|
setTimeout(() => {
|
||||||
|
this.confetti.stop()
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async checkSubscription() {
|
async checkSubscription() {
|
||||||
|
|
@ -73,6 +81,7 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
this.confetti.play()
|
||||||
this.$router.push({name: "home"})
|
this.$router.push({name: "home"})
|
||||||
|
|
||||||
if (this.user.has_enterprise_subscription) {
|
if (this.user.has_enterprise_subscription) {
|
||||||
|
|
@ -88,6 +97,6 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
|
function parseBoolean(value, defaultValue = false) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.toLowerCase().trim()
|
||||||
|
if (value === 'true' || value === '1') return true
|
||||||
|
if (value === 'false' || value === '0') return false
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber(value, defaultValue = 0) {
|
||||||
|
const parsedValue = parseFloat(value)
|
||||||
|
return isNaN(parsedValue) ? defaultValue : parsedValue
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Keys within public, will be also exposed to the client-side
|
// Keys within public, will be also exposed to the client-side
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE ||'',
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || '',
|
||||||
appUrl: process.env.NUXT_PUBLIC_APP_URL || '',
|
appUrl: process.env.NUXT_PUBLIC_APP_URL || '',
|
||||||
env: process.env.NUXT_PUBLIC_ENV || 'local',
|
env: process.env.NUXT_PUBLIC_ENV || 'local',
|
||||||
hCaptchaSiteKey: process.env.NUXT_PUBLIC_H_CAPTCHA_SITE_KEY || null,
|
hCaptchaSiteKey: process.env.NUXT_PUBLIC_H_CAPTCHA_SITE_KEY || null,
|
||||||
gtmCode: process.env.NUXT_PUBLIC_GTM_CODE || null,
|
gtmCode: process.env.NUXT_PUBLIC_GTM_CODE || null,
|
||||||
amplitudeCode: process.env.NUXT_PUBLIC_AMPLITUDE_CODE || null,
|
amplitudeCode: process.env.NUXT_PUBLIC_AMPLITUDE_CODE || null,
|
||||||
crispWebsiteId: process.env.NUXT_PUBLIC_CRISP_WEBSITE_ID || null,
|
crispWebsiteId: process.env.NUXT_PUBLIC_CRISP_WEBSITE_ID || null,
|
||||||
aiFeaturesEnabled: process.env.NUXT_PUBLIC_AI_FEATURES_ENABLED || false,
|
aiFeaturesEnabled: parseBoolean(process.env.NUXT_PUBLIC_AI_FEATURES_ENABLED),
|
||||||
s3Enabled: process.env.NUXT_PUBLIC_S3_ENABLED || false,
|
s3Enabled: parseBoolean(process.env.NUXT_PUBLIC_S3_ENABLED),
|
||||||
paidPlansEnabled: process.env.NUXT_PUBLIC_PAID_PLANS_ENABLED || false,
|
paidPlansEnabled: parseBoolean(process.env.NUXT_PUBLIC_PAID_PLANS_ENABLED),
|
||||||
customDomainsEnabled: process.env.NUXT_PUBLIC_CUSTOM_DOMAINS_ENABLED || false,
|
customDomainsEnabled: parseBoolean(process.env.NUXT_PUBLIC_CUSTOM_DOMAINS_ENABLED),
|
||||||
featureBaseOrganization: process.env.NUXT_PUBLIC_FEATURE_BASE_ORGANISATION || null,
|
featureBaseOrganization: process.env.NUXT_PUBLIC_FEATURE_BASE_ORGANISATION || null,
|
||||||
|
selfHosted: parseBoolean(process.env.NUXT_PUBLIC_SELF_HOSTED, true),
|
||||||
|
|
||||||
// Config within public will be also exposed to the client
|
// Config within public will be also exposed to the client
|
||||||
SENTRY_DSN_PUBLIC: process.env.SENTRY_DSN_PUBLIC,
|
SENTRY_DSN_PUBLIC: process.env.SENTRY_DSN_PUBLIC,
|
||||||
SENTRY_TRACES_SAMPLE_RATE: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0'),
|
SENTRY_TRACES_SAMPLE_RATE: parseNumber(process.env.SENTRY_TRACES_SAMPLE_RATE),
|
||||||
SENTRY_REPLAY_SAMPLE_RATE: parseFloat(process.env.SENTRY_REPLAY_SAMPLE_RATE ?? '0'),
|
SENTRY_REPLAY_SAMPLE_RATE: parseNumber(process.env.SENTRY_REPLAY_SAMPLE_RATE),
|
||||||
SENTRY_ERROR_REPLAY_SAMPLE_RATE: parseFloat(process.env.SENTRY_ERROR_REPLAY_SAMPLE_RATE ?? '0'),
|
SENTRY_ERROR_REPLAY_SAMPLE_RATE: parseNumber(process.env.SENTRY_ERROR_REPLAY_SAMPLE_RATE),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,6 @@ body.dark * {
|
||||||
--bg-form-color: #2563eb;
|
--bg-form-color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
p, div {
|
|
||||||
@apply text-gray-900 dark:text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
@apply text-gray-900 dark:text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply text-3xl sm:text-4xl font-semibold;
|
@apply text-3xl sm:text-4xl font-semibold;
|
||||||
}
|
}
|
||||||
|
|
@ -37,10 +29,6 @@ body.dark * {
|
||||||
h2 {
|
h2 {
|
||||||
@apply text-3xl font-semibold;
|
@apply text-3xl font-semibold;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
@apply text-blue-600 hover:underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-white {
|
.bg-white {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,14 @@ export const useWorkspacesStore = defineStore("workspaces", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getWorkspaceUsers = async() => {
|
||||||
|
return await opnFetch(`${workspaceEndpoint}${currentId.value}/users/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWorkspaceInvites = async() => {
|
||||||
|
return await opnFetch(`${workspaceEndpoint}${currentId.value}/invites/`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...contentStore,
|
...contentStore,
|
||||||
currentId,
|
currentId,
|
||||||
|
|
@ -47,6 +55,8 @@ export const useWorkspacesStore = defineStore("workspaces", () => {
|
||||||
set,
|
set,
|
||||||
save,
|
save,
|
||||||
remove,
|
remove,
|
||||||
|
getWorkspaceUsers,
|
||||||
|
getWorkspaceInvites,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -921,16 +921,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "aws/aws-sdk-php",
|
"name": "aws/aws-sdk-php",
|
||||||
"version": "3.310.0",
|
"version": "3.314.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||||
"reference": "8ac02d36c609c6507136e5996f60cfd5152b4fd7"
|
"reference": "c9e8a31cfa07f47b7ab9ecc741845a3a9d50fc61"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8ac02d36c609c6507136e5996f60cfd5152b4fd7",
|
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c9e8a31cfa07f47b7ab9ecc741845a3a9d50fc61",
|
||||||
"reference": "8ac02d36c609c6507136e5996f60cfd5152b4fd7",
|
"reference": "c9e8a31cfa07f47b7ab9ecc741845a3a9d50fc61",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -1010,9 +1010,9 @@
|
||||||
"support": {
|
"support": {
|
||||||
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
|
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
|
||||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.310.0"
|
"source": "https://github.com/aws/aws-sdk-php/tree/3.314.3"
|
||||||
},
|
},
|
||||||
"time": "2024-06-04T18:05:36+00:00"
|
"time": "2024-06-17T18:13:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
|
@ -1345,16 +1345,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/dbal",
|
"name": "doctrine/dbal",
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/doctrine/dbal.git",
|
"url": "https://github.com/doctrine/dbal.git",
|
||||||
"reference": "61d79c6e379a39dc1fea6b4e50a23dfc3cd2076a"
|
"reference": "8edbce73bc1aa2251ba8c754fc440f8e02c661bc"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/61d79c6e379a39dc1fea6b4e50a23dfc3cd2076a",
|
"url": "https://api.github.com/repos/doctrine/dbal/zipball/8edbce73bc1aa2251ba8c754fc440f8e02c661bc",
|
||||||
"reference": "61d79c6e379a39dc1fea6b4e50a23dfc3cd2076a",
|
"reference": "8edbce73bc1aa2251ba8c754fc440f8e02c661bc",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -1367,16 +1367,16 @@
|
||||||
"doctrine/coding-standard": "12.0.0",
|
"doctrine/coding-standard": "12.0.0",
|
||||||
"fig/log-test": "^1",
|
"fig/log-test": "^1",
|
||||||
"jetbrains/phpstorm-stubs": "2023.2",
|
"jetbrains/phpstorm-stubs": "2023.2",
|
||||||
"phpstan/phpstan": "1.10.58",
|
"phpstan/phpstan": "1.11.1",
|
||||||
"phpstan/phpstan-phpunit": "1.3.15",
|
"phpstan/phpstan-phpunit": "1.4.0",
|
||||||
"phpstan/phpstan-strict-rules": "^1.5",
|
"phpstan/phpstan-strict-rules": "^1.6",
|
||||||
"phpunit/phpunit": "10.5.9",
|
"phpunit/phpunit": "10.5.20",
|
||||||
"psalm/plugin-phpunit": "0.18.4",
|
"psalm/plugin-phpunit": "0.19.0",
|
||||||
"slevomat/coding-standard": "8.13.1",
|
"slevomat/coding-standard": "8.13.1",
|
||||||
"squizlabs/php_codesniffer": "3.9.0",
|
"squizlabs/php_codesniffer": "3.9.2",
|
||||||
"symfony/cache": "^6.3.8|^7.0",
|
"symfony/cache": "^6.3.8|^7.0",
|
||||||
"symfony/console": "^5.4|^6.3|^7.0",
|
"symfony/console": "^5.4|^6.3|^7.0",
|
||||||
"vimeo/psalm": "5.21.1"
|
"vimeo/psalm": "5.24.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"symfony/console": "For helpful console commands such as SQL execution and import of files."
|
"symfony/console": "For helpful console commands such as SQL execution and import of files."
|
||||||
|
|
@ -1433,7 +1433,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/doctrine/dbal/issues",
|
"issues": "https://github.com/doctrine/dbal/issues",
|
||||||
"source": "https://github.com/doctrine/dbal/tree/4.0.2"
|
"source": "https://github.com/doctrine/dbal/tree/4.0.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -1449,7 +1449,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-04-25T08:29:52+00:00"
|
"time": "2024-06-12T06:58:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/deprecations",
|
"name": "doctrine/deprecations",
|
||||||
|
|
@ -1991,16 +1991,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "giggsey/libphonenumber-for-php",
|
"name": "giggsey/libphonenumber-for-php",
|
||||||
"version": "8.13.37",
|
"version": "8.13.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/giggsey/libphonenumber-for-php.git",
|
"url": "https://github.com/giggsey/libphonenumber-for-php.git",
|
||||||
"reference": "536c747ff1af433dddc615b26b9674047e013076"
|
"reference": "5a36692616dba1ec4a24217f248021b1ec9cdade"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/536c747ff1af433dddc615b26b9674047e013076",
|
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/5a36692616dba1ec4a24217f248021b1ec9cdade",
|
||||||
"reference": "536c747ff1af433dddc615b26b9674047e013076",
|
"reference": "5a36692616dba1ec4a24217f248021b1ec9cdade",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -2062,7 +2062,7 @@
|
||||||
"issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
|
"issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
|
||||||
"source": "https://github.com/giggsey/libphonenumber-for-php"
|
"source": "https://github.com/giggsey/libphonenumber-for-php"
|
||||||
},
|
},
|
||||||
"time": "2024-05-16T09:01:39+00:00"
|
"time": "2024-06-14T12:43:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "giggsey/locale",
|
"name": "giggsey/locale",
|
||||||
|
|
@ -2189,16 +2189,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google/apiclient-services",
|
"name": "google/apiclient-services",
|
||||||
"version": "v0.358.0",
|
"version": "v0.360.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/googleapis/google-api-php-client-services.git",
|
"url": "https://github.com/googleapis/google-api-php-client-services.git",
|
||||||
"reference": "a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd"
|
"reference": "e48813050e660c7dcbe48cb6556461efe6381a54"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd",
|
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/e48813050e660c7dcbe48cb6556461efe6381a54",
|
||||||
"reference": "a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd",
|
"reference": "e48813050e660c7dcbe48cb6556461efe6381a54",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -2227,9 +2227,9 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
|
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
|
||||||
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.358.0"
|
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.360.0"
|
||||||
},
|
},
|
||||||
"time": "2024-06-03T01:02:16+00:00"
|
"time": "2024-06-17T01:06:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google/auth",
|
"name": "google/auth",
|
||||||
|
|
@ -5853,16 +5853,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openai-php/client",
|
"name": "openai-php/client",
|
||||||
"version": "v0.9.2",
|
"version": "v0.10.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/openai-php/client.git",
|
"url": "https://github.com/openai-php/client.git",
|
||||||
"reference": "a4e519a3efa380c1dc9d64b1fd1fecf8fc466ba2"
|
"reference": "8b63d27a2f009a7ce4714fda77769e93d883c8da"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/openai-php/client/zipball/a4e519a3efa380c1dc9d64b1fd1fecf8fc466ba2",
|
"url": "https://api.github.com/repos/openai-php/client/zipball/8b63d27a2f009a7ce4714fda77769e93d883c8da",
|
||||||
"reference": "a4e519a3efa380c1dc9d64b1fd1fecf8fc466ba2",
|
"reference": "8b63d27a2f009a7ce4714fda77769e93d883c8da",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -5925,7 +5925,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/openai-php/client/issues",
|
"issues": "https://github.com/openai-php/client/issues",
|
||||||
"source": "https://github.com/openai-php/client/tree/v0.9.2"
|
"source": "https://github.com/openai-php/client/tree/v0.10.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -5941,28 +5941,28 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-27T19:27:25+00:00"
|
"time": "2024-06-06T20:27:51+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "paragonie/constant_time_encoding",
|
"name": "paragonie/constant_time_encoding",
|
||||||
"version": "v2.7.0",
|
"version": "v3.0.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/paragonie/constant_time_encoding.git",
|
"url": "https://github.com/paragonie/constant_time_encoding.git",
|
||||||
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105"
|
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105",
|
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
|
||||||
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105",
|
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7|^8"
|
"php": "^8"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^6|^7|^8|^9",
|
"phpunit/phpunit": "^9",
|
||||||
"vimeo/psalm": "^1|^2|^3|^4"
|
"vimeo/psalm": "^4|^5"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|
@ -6008,7 +6008,7 @@
|
||||||
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
|
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
|
||||||
"source": "https://github.com/paragonie/constant_time_encoding"
|
"source": "https://github.com/paragonie/constant_time_encoding"
|
||||||
},
|
},
|
||||||
"time": "2024-05-08T12:18:48+00:00"
|
"time": "2024-05-08T12:36:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "paragonie/random_compat",
|
"name": "paragonie/random_compat",
|
||||||
|
|
@ -6141,16 +6141,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "php-http/multipart-stream-builder",
|
"name": "php-http/multipart-stream-builder",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/php-http/multipart-stream-builder.git",
|
"url": "https://github.com/php-http/multipart-stream-builder.git",
|
||||||
"reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a"
|
"reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a",
|
"url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/ed56da23b95949ae4747378bed8a5b61a2fdae24",
|
||||||
"reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a",
|
"reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -6191,9 +6191,9 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/php-http/multipart-stream-builder/issues",
|
"issues": "https://github.com/php-http/multipart-stream-builder/issues",
|
||||||
"source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0"
|
"source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.1"
|
||||||
},
|
},
|
||||||
"time": "2023-04-28T14:10:22+00:00"
|
"time": "2024-06-10T14:51:55+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpdocumentor/reflection-common",
|
"name": "phpdocumentor/reflection-common",
|
||||||
|
|
@ -6488,20 +6488,20 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpseclib/phpseclib",
|
"name": "phpseclib/phpseclib",
|
||||||
"version": "3.0.37",
|
"version": "3.0.38",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||||
"reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8"
|
"reference": "b18b8788e51156c4dd97b7f220a31149a0052067"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
|
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b18b8788e51156c4dd97b7f220a31149a0052067",
|
||||||
"reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
|
"reference": "b18b8788e51156c4dd97b7f220a31149a0052067",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"paragonie/constant_time_encoding": "^1|^2",
|
"paragonie/constant_time_encoding": "^1|^2|^3",
|
||||||
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||||
"php": ">=5.6.1"
|
"php": ">=5.6.1"
|
||||||
},
|
},
|
||||||
|
|
@ -6578,7 +6578,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.37"
|
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.38"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -6594,7 +6594,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-03-03T02:14:58+00:00"
|
"time": "2024-06-17T10:11:32+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpdoc-parser",
|
"name": "phpstan/phpdoc-parser",
|
||||||
|
|
@ -7183,16 +7183,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psy/psysh",
|
"name": "psy/psysh",
|
||||||
"version": "v0.12.3",
|
"version": "v0.12.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/bobthecow/psysh.git",
|
"url": "https://github.com/bobthecow/psysh.git",
|
||||||
"reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73"
|
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
|
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818",
|
||||||
"reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
|
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -7256,9 +7256,9 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/bobthecow/psysh/issues",
|
"issues": "https://github.com/bobthecow/psysh/issues",
|
||||||
"source": "https://github.com/bobthecow/psysh/tree/v0.12.3"
|
"source": "https://github.com/bobthecow/psysh/tree/v0.12.4"
|
||||||
},
|
},
|
||||||
"time": "2024-04-02T15:57:53+00:00"
|
"time": "2024-06-10T01:18:23+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ralouphie/getallheaders",
|
"name": "ralouphie/getallheaders",
|
||||||
|
|
@ -7704,16 +7704,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sentry/sentry-laravel",
|
"name": "sentry/sentry-laravel",
|
||||||
"version": "4.5.1",
|
"version": "4.6.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/getsentry/sentry-laravel.git",
|
"url": "https://github.com/getsentry/sentry-laravel.git",
|
||||||
"reference": "a15b2f5fa7b446f006eb93134e237c38a11776cf"
|
"reference": "75c11944211ce7707bb92e717c5bda93a1759438"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/a15b2f5fa7b446f006eb93134e237c38a11776cf",
|
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/75c11944211ce7707bb92e717c5bda93a1759438",
|
||||||
"reference": "a15b2f5fa7b446f006eb93134e237c38a11776cf",
|
"reference": "75c11944211ce7707bb92e717c5bda93a1759438",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -7777,7 +7777,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/getsentry/sentry-laravel/issues",
|
"issues": "https://github.com/getsentry/sentry-laravel/issues",
|
||||||
"source": "https://github.com/getsentry/sentry-laravel/tree/4.5.1"
|
"source": "https://github.com/getsentry/sentry-laravel/tree/4.6.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -7789,20 +7789,20 @@
|
||||||
"type": "custom"
|
"type": "custom"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-04-24T08:58:34+00:00"
|
"time": "2024-06-11T12:23:24+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/browsershot",
|
"name": "spatie/browsershot",
|
||||||
"version": "4.0.5",
|
"version": "4.1.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/browsershot.git",
|
"url": "https://github.com/spatie/browsershot.git",
|
||||||
"reference": "a080a60b6fc8dc359aaebd761eb107985e08e340"
|
"reference": "1fbc5955a24ec9b4dbc2620f78c03d5c043856d2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/browsershot/zipball/a080a60b6fc8dc359aaebd761eb107985e08e340",
|
"url": "https://api.github.com/repos/spatie/browsershot/zipball/1fbc5955a24ec9b4dbc2620f78c03d5c043856d2",
|
||||||
"reference": "a080a60b6fc8dc359aaebd761eb107985e08e340",
|
"reference": "1fbc5955a24ec9b4dbc2620f78c03d5c043856d2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -7848,7 +7848,7 @@
|
||||||
"webpage"
|
"webpage"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/spatie/browsershot/tree/4.0.5"
|
"source": "https://github.com/spatie/browsershot/tree/4.1.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -7856,7 +7856,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-06-04T11:11:30+00:00"
|
"time": "2024-06-12T07:42:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/crawler",
|
"name": "spatie/crawler",
|
||||||
|
|
@ -7928,16 +7928,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-data",
|
"name": "spatie/laravel-data",
|
||||||
"version": "4.6.0",
|
"version": "4.7.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/laravel-data.git",
|
"url": "https://github.com/spatie/laravel-data.git",
|
||||||
"reference": "ee513f693f8ab8f915dc26cae079d7b2e5999a65"
|
"reference": "92af136b14f57c72b1b8e36cd5f7e274fb56385a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/laravel-data/zipball/ee513f693f8ab8f915dc26cae079d7b2e5999a65",
|
"url": "https://api.github.com/repos/spatie/laravel-data/zipball/92af136b14f57c72b1b8e36cd5f7e274fb56385a",
|
||||||
"reference": "ee513f693f8ab8f915dc26cae079d7b2e5999a65",
|
"reference": "92af136b14f57c72b1b8e36cd5f7e274fb56385a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -7950,7 +7950,7 @@
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.14",
|
"fakerphp/faker": "^1.14",
|
||||||
"friendsofphp/php-cs-fixer": "^3.0",
|
"friendsofphp/php-cs-fixer": "^3.0",
|
||||||
"inertiajs/inertia-laravel": "dev-master#4508fd1",
|
"inertiajs/inertia-laravel": "^1.2",
|
||||||
"livewire/livewire": "^3.0",
|
"livewire/livewire": "^3.0",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nesbot/carbon": "^2.63",
|
"nesbot/carbon": "^2.63",
|
||||||
|
|
@ -8000,7 +8000,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/spatie/laravel-data/issues",
|
"issues": "https://github.com/spatie/laravel-data/issues",
|
||||||
"source": "https://github.com/spatie/laravel-data/tree/4.6.0"
|
"source": "https://github.com/spatie/laravel-data/tree/4.7.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -8008,7 +8008,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-03T15:01:04+00:00"
|
"time": "2024-06-13T12:07:24+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-package-tools",
|
"name": "spatie/laravel-package-tools",
|
||||||
|
|
@ -11865,16 +11865,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "composer/class-map-generator",
|
"name": "composer/class-map-generator",
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/composer/class-map-generator.git",
|
"url": "https://github.com/composer/class-map-generator.git",
|
||||||
"reference": "acd227952154850d0bb7d65caa4f9edf9cd806a7"
|
"reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/acd227952154850d0bb7d65caa4f9edf9cd806a7",
|
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3",
|
||||||
"reference": "acd227952154850d0bb7d65caa4f9edf9cd806a7",
|
"reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -11918,7 +11918,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/composer/class-map-generator/issues",
|
"issues": "https://github.com/composer/class-map-generator/issues",
|
||||||
"source": "https://github.com/composer/class-map-generator/tree/1.3.2"
|
"source": "https://github.com/composer/class-map-generator/tree/1.3.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -11934,7 +11934,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-31T19:45:56+00:00"
|
"time": "2024-06-12T14:13:04+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "composer/pcre",
|
"name": "composer/pcre",
|
||||||
|
|
@ -12539,16 +12539,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
"version": "1.11.1",
|
"version": "1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
|
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
|
||||||
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
|
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -12556,11 +12556,12 @@
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"doctrine/collections": "<1.6.8",
|
"doctrine/collections": "<1.6.8",
|
||||||
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/collections": "^1.6.8",
|
"doctrine/collections": "^1.6.8",
|
||||||
"doctrine/common": "^2.13.3 || ^3.2.2",
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
|
|
@ -12586,7 +12587,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -12594,7 +12595,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-03-08T13:26:56+00:00"
|
"time": "2024-06-12T14:39:25+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nunomaduro/collision",
|
"name": "nunomaduro/collision",
|
||||||
|
|
@ -12695,16 +12696,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pestphp/pest",
|
"name": "pestphp/pest",
|
||||||
"version": "v2.34.7",
|
"version": "v2.34.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/pestphp/pest.git",
|
"url": "https://github.com/pestphp/pest.git",
|
||||||
"reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76"
|
"reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/pestphp/pest/zipball/a7a3e4240e341d0fee1c54814ce18adc26ce5a76",
|
"url": "https://api.github.com/repos/pestphp/pest/zipball/e8f122bf47585c06431e0056189ec6bfd6f41f57",
|
||||||
"reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76",
|
"reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -12723,8 +12724,8 @@
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^2.16.0",
|
"pestphp/pest-dev-tools": "^2.16.0",
|
||||||
"pestphp/pest-plugin-type-coverage": "^2.8.1",
|
"pestphp/pest-plugin-type-coverage": "^2.8.3",
|
||||||
"symfony/process": "^6.4.0|^7.0.4"
|
"symfony/process": "^6.4.0|^7.1.1"
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
"bin/pest"
|
"bin/pest"
|
||||||
|
|
@ -12787,7 +12788,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/pestphp/pest/issues",
|
"issues": "https://github.com/pestphp/pest/issues",
|
||||||
"source": "https://github.com/pestphp/pest/tree/v2.34.7"
|
"source": "https://github.com/pestphp/pest/tree/v2.34.8"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -12799,7 +12800,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-04-05T07:44:17+00:00"
|
"time": "2024-06-10T22:02:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pestphp/pest-plugin",
|
"name": "pestphp/pest-plugin",
|
||||||
|
|
@ -13192,16 +13193,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "1.11.3",
|
"version": "1.11.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpstan/phpstan.git",
|
"url": "https://github.com/phpstan/phpstan.git",
|
||||||
"reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5"
|
"reference": "490f0ae1c92b082f154681d7849aee776a7c1443"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e64220a05c1209fc856d58e789c3b7a32c0bb9a5",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/490f0ae1c92b082f154681d7849aee776a7c1443",
|
||||||
"reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5",
|
"reference": "490f0ae1c92b082f154681d7849aee776a7c1443",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -13246,7 +13247,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-31T13:53:37+00:00"
|
"time": "2024-06-17T15:10:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
|
|
@ -14762,23 +14763,97 @@
|
||||||
"time": "2024-04-24T13:22:11+00:00"
|
"time": "2024-04-24T13:22:11+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/flare-client-php",
|
"name": "spatie/error-solutions",
|
||||||
"version": "1.6.0",
|
"version": "1.0.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/flare-client-php.git",
|
"url": "https://github.com/spatie/error-solutions.git",
|
||||||
"reference": "220a7c8745e9fa427d54099f47147c4b97fe6462"
|
"reference": "202108314a6988ede156fba1b3ea80a784c1734a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/220a7c8745e9fa427d54099f47147c4b97fe6462",
|
"url": "https://api.github.com/repos/spatie/error-solutions/zipball/202108314a6988ede156fba1b3ea80a784c1734a",
|
||||||
"reference": "220a7c8745e9fa427d54099f47147c4b97fe6462",
|
"reference": "202108314a6988ede156fba1b3ea80a784c1734a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"illuminate/broadcasting": "^10.0|^11.0",
|
||||||
|
"illuminate/cache": "^10.0|^11.0",
|
||||||
|
"illuminate/support": "^10.0|^11.0",
|
||||||
|
"livewire/livewire": "^2.11|^3.3.5",
|
||||||
|
"openai-php/client": "^0.10.1",
|
||||||
|
"orchestra/testbench": "^7.0|8.22.3|^9.0",
|
||||||
|
"pestphp/pest": "^2.20",
|
||||||
|
"phpstan/phpstan": "^1.11",
|
||||||
|
"psr/simple-cache": "^3.0",
|
||||||
|
"psr/simple-cache-implementation": "^3.0",
|
||||||
|
"spatie/ray": "^1.28",
|
||||||
|
"symfony/cache": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/process": "^5.4|^6.0|^7.0",
|
||||||
|
"vlucas/phpdotenv": "^5.5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"openai-php/client": "Require get solutions from OpenAI",
|
||||||
|
"simple-cache-implementation": "To cache solutions from OpenAI"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Ignition\\": "legacy/ignition",
|
||||||
|
"Spatie\\ErrorSolutions\\": "src",
|
||||||
|
"Spatie\\LaravelIgnition\\": "legacy/laravel-ignition"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ruben Van Assche",
|
||||||
|
"email": "ruben@spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This is my package error-solutions",
|
||||||
|
"homepage": "https://github.com/spatie/error-solutions",
|
||||||
|
"keywords": [
|
||||||
|
"error-solutions",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/error-solutions/issues",
|
||||||
|
"source": "https://github.com/spatie/error-solutions/tree/1.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-06-12T14:49:54+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/flare-client-php",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/flare-client-php.git",
|
||||||
|
"reference": "097040ff51e660e0f6fc863684ac4b02c93fa234"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/097040ff51e660e0f6fc863684ac4b02c93fa234",
|
||||||
|
"reference": "097040ff51e660e0f6fc863684ac4b02c93fa234",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0",
|
"illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0",
|
||||||
"php": "^8.0",
|
"php": "^8.0",
|
||||||
"spatie/backtrace": "^1.5.2",
|
"spatie/backtrace": "^1.6.1",
|
||||||
"symfony/http-foundation": "^5.2|^6.0|^7.0",
|
"symfony/http-foundation": "^5.2|^6.0|^7.0",
|
||||||
"symfony/mime": "^5.2|^6.0|^7.0",
|
"symfony/mime": "^5.2|^6.0|^7.0",
|
||||||
"symfony/process": "^5.2|^6.0|^7.0",
|
"symfony/process": "^5.2|^6.0|^7.0",
|
||||||
|
|
@ -14820,7 +14895,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/spatie/flare-client-php/issues",
|
"issues": "https://github.com/spatie/flare-client-php/issues",
|
||||||
"source": "https://github.com/spatie/flare-client-php/tree/1.6.0"
|
"source": "https://github.com/spatie/flare-client-php/tree/1.7.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -14828,28 +14903,28 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-22T09:45:39+00:00"
|
"time": "2024-06-12T14:39:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/ignition",
|
"name": "spatie/ignition",
|
||||||
"version": "1.14.2",
|
"version": "1.15.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/ignition.git",
|
"url": "https://github.com/spatie/ignition.git",
|
||||||
"reference": "5e11c11f675bb5251f061491a493e04a1a571532"
|
"reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/ignition/zipball/5e11c11f675bb5251f061491a493e04a1a571532",
|
"url": "https://api.github.com/repos/spatie/ignition/zipball/e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
|
||||||
"reference": "5e11c11f675bb5251f061491a493e04a1a571532",
|
"reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"php": "^8.0",
|
"php": "^8.0",
|
||||||
"spatie/backtrace": "^1.5.3",
|
"spatie/error-solutions": "^1.0",
|
||||||
"spatie/flare-client-php": "^1.4.0",
|
"spatie/flare-client-php": "^1.7",
|
||||||
"symfony/console": "^5.4|^6.0|^7.0",
|
"symfony/console": "^5.4|^6.0|^7.0",
|
||||||
"symfony/var-dumper": "^5.4|^6.0|^7.0"
|
"symfony/var-dumper": "^5.4|^6.0|^7.0"
|
||||||
},
|
},
|
||||||
|
|
@ -14911,20 +14986,20 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-29T08:10:20+00:00"
|
"time": "2024-06-12T14:55:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-ignition",
|
"name": "spatie/laravel-ignition",
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/laravel-ignition.git",
|
"url": "https://github.com/spatie/laravel-ignition.git",
|
||||||
"reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57"
|
"reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/f52124d50122611e8a40f628cef5c19ff6cc5b57",
|
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/3c067b75bfb50574db8f7e2c3978c65eed71126c",
|
||||||
"reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57",
|
"reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -14933,8 +15008,7 @@
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"illuminate/support": "^10.0|^11.0",
|
"illuminate/support": "^10.0|^11.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/flare-client-php": "^1.5",
|
"spatie/ignition": "^1.15",
|
||||||
"spatie/ignition": "^1.14",
|
|
||||||
"symfony/console": "^6.2.3|^7.0",
|
"symfony/console": "^6.2.3|^7.0",
|
||||||
"symfony/var-dumper": "^6.2.3|^7.0"
|
"symfony/var-dumper": "^6.2.3|^7.0"
|
||||||
},
|
},
|
||||||
|
|
@ -15003,7 +15077,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-05-02T13:42:49+00:00"
|
"time": "2024-06-12T15:01:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-ray",
|
"name": "spatie/laravel-ray",
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ return [
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
'asset_url' => env('ASSET_URL', null),
|
'asset_url' => env('ASSET_URL', null),
|
||||||
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ return [
|
||||||
'yearly' => env('STRIPE_PROD_DEFAULT_PRICING_YEARLY'),
|
'yearly' => env('STRIPE_PROD_DEFAULT_PRICING_YEARLY'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'extra_user' => [
|
||||||
|
'product_id' => env('STRIPE_PROD_EXTRA_USER_PRODUCT_ID'),
|
||||||
|
'pricing' => [
|
||||||
|
'monthly' => env('STRIPE_PROD_EXTRA_USER_PRICING_MONTHLY'),
|
||||||
|
'yearly' => env('STRIPE_PROD_EXTRA_USER_PRICING_YEARLY'),
|
||||||
|
],
|
||||||
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
'test' => [
|
'test' => [
|
||||||
|
|
@ -20,6 +28,14 @@ return [
|
||||||
'yearly' => env('STRIPE_TEST_DEFAULT_PRICING_YEARLY'),
|
'yearly' => env('STRIPE_TEST_DEFAULT_PRICING_YEARLY'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'extra_user' => [
|
||||||
|
'product_id' => env('STRIPE_TEST_EXTRA_USER_PRODUCT_ID'),
|
||||||
|
'pricing' => [
|
||||||
|
'monthly' => env('STRIPE_TEST_EXTRA_USER_PRICING_MONTHLY'),
|
||||||
|
'yearly' => env('STRIPE_TEST_EXTRA_USER_PRICING_YEARLY'),
|
||||||
|
],
|
||||||
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
'discount_coupon_id' => env('STRIPE_DISCOUNT_COUPON_ID', null),
|
'discount_coupon_id' => env('STRIPE_DISCOUNT_COUPON_ID', null),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Workspace>
|
||||||
|
*/
|
||||||
|
class WorkspaceFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->company,
|
||||||
|
'icon' => '🧪',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class () extends Migration {
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('user_invites', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(\App\Models\Workspace::class, 'workspace_id');
|
||||||
|
$table->string('email');
|
||||||
|
$table->string('role')->default('user');
|
||||||
|
$table->string('token', 191)->unique();
|
||||||
|
$table->string('status')->default(\App\Models\UserInvite::PENDING_STATUS);
|
||||||
|
$table->dateTime('valid_until')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_invites');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
<env name="MAIL_FROM_NAME" value="NotionForms"/>
|
<env name="MAIL_FROM_NAME" value="NotionForms"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="SELF_HOSTED" value="false"/>
|
||||||
<env name="TEMPLATE_EDITOR_EMAILS" value="admin@opnform.com"/>
|
<env name="TEMPLATE_EDITOR_EMAILS" value="admin@opnform.com"/>
|
||||||
<env name="JWT_SECRET" value="9K6whOetAFaokQgSIdbMQZuJuDV5uS2Y"/>
|
<env name="JWT_SECRET" value="9K6whOetAFaokQgSIdbMQZuJuDV5uS2Y"/>
|
||||||
</php>
|
</php>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
@component('mail::message')
|
||||||
|
|
||||||
|
# Invitation to Join {{ $workspaceName }} on OpnForm
|
||||||
|
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
You have been invited to join the workspace "{{ $workspaceName }}" on OpnForm, a platform that simplifies form creation and data collection. With OpnForm, you can easily create, distribute, and manage forms for any purpose.
|
||||||
|
|
||||||
|
To join us, please click the button below.
|
||||||
|
|
||||||
|
@component('mail::button', ['url' => $inviteLink])
|
||||||
|
Accept Invitation
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
Looking forward to having you on board.
|
||||||
|
|
||||||
|
Best Regards,
|
||||||
|
The OpnForm Team
|
||||||
|
|
||||||
|
@endcomponent
|
||||||
|
|
@ -20,7 +20,9 @@ use App\Http\Controllers\Settings\PasswordController;
|
||||||
use App\Http\Controllers\Settings\ProfileController;
|
use App\Http\Controllers\Settings\ProfileController;
|
||||||
use App\Http\Controllers\SubscriptionController;
|
use App\Http\Controllers\SubscriptionController;
|
||||||
use App\Http\Controllers\TemplateController;
|
use App\Http\Controllers\TemplateController;
|
||||||
|
use App\Http\Controllers\UserInviteController;
|
||||||
use App\Http\Controllers\WorkspaceController;
|
use App\Http\Controllers\WorkspaceController;
|
||||||
|
use App\Http\Controllers\WorkspaceUserController;
|
||||||
use App\Http\Middleware\Form\ResolveFormMiddleware;
|
use App\Http\Middleware\Form\ResolveFormMiddleware;
|
||||||
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
|
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -62,6 +64,7 @@ Route::group(['middleware' => 'auth:api'], function () {
|
||||||
->where('subscription', '(' . implode('|', SubscriptionController::SUBSCRIPTION_NAMES) . ')')
|
->where('subscription', '(' . implode('|', SubscriptionController::SUBSCRIPTION_NAMES) . ')')
|
||||||
->where('plan', '(' . implode('|', SubscriptionController::SUBSCRIPTION_PLANS) . ')');
|
->where('plan', '(' . implode('|', SubscriptionController::SUBSCRIPTION_PLANS) . ')');
|
||||||
Route::get('/billing-portal', [SubscriptionController::class, 'billingPortal'])->name('billing-portal');
|
Route::get('/billing-portal', [SubscriptionController::class, 'billingPortal'])->name('billing-portal');
|
||||||
|
Route::get('/users-count', [SubscriptionController::class, 'getUsersCount'])->name('users-count');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('open')->name('open.')->group(function () {
|
Route::prefix('open')->name('open.')->group(function () {
|
||||||
|
|
@ -77,8 +80,43 @@ Route::group(['middleware' => 'auth:api'], function () {
|
||||||
Route::prefix('/{workspaceId}')->group(function () {
|
Route::prefix('/{workspaceId}')->group(function () {
|
||||||
Route::get(
|
Route::get(
|
||||||
'/users',
|
'/users',
|
||||||
[WorkspaceController::class, 'listUsers']
|
[WorkspaceUserController::class, 'listUsers']
|
||||||
)->name('users.index');
|
)->name('users.index');
|
||||||
|
Route::get(
|
||||||
|
'/invites',
|
||||||
|
[UserInviteController::class, 'listInvites']
|
||||||
|
)->name('invites.index');
|
||||||
|
|
||||||
|
Route::post(
|
||||||
|
'/users/add',
|
||||||
|
[WorkspaceUserController::class, 'addUser']
|
||||||
|
)->name('users.add');
|
||||||
|
|
||||||
|
Route::delete(
|
||||||
|
'/users/{userId}/remove',
|
||||||
|
[WorkspaceUserController::class, 'removeUser']
|
||||||
|
)->name('users.remove');
|
||||||
|
|
||||||
|
Route::post(
|
||||||
|
'/invites/{inviteId}/resend',
|
||||||
|
[UserInviteController::class, 'resendInvite']
|
||||||
|
)->name('invites.resend');
|
||||||
|
|
||||||
|
Route::delete(
|
||||||
|
'/invites/{inviteId}/cancel',
|
||||||
|
[UserInviteController::class, 'cancelInvite']
|
||||||
|
)->name('invites.cancel');
|
||||||
|
|
||||||
|
Route::put(
|
||||||
|
'/users/{userId}/update-role',
|
||||||
|
[WorkspaceUserController::class, 'updateUserRole']
|
||||||
|
)->name('users.update-role');
|
||||||
|
|
||||||
|
// leave workspace route
|
||||||
|
Route::post(
|
||||||
|
'/leave',
|
||||||
|
[WorkspaceUserController::class, 'leaveWorkspace']
|
||||||
|
)->name('leave');
|
||||||
|
|
||||||
Route::prefix('/databases')->name('databases.')->group(function () {
|
Route::prefix('/databases')->name('databases.')->group(function () {
|
||||||
Route::get(
|
Route::get(
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ it('check formstat chart data', function () {
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
->where('submissions', function ($values) use ($submissions) {
|
->where('submissions', function ($values) use ($submissions) {
|
||||||
ray($values, $submissions);
|
|
||||||
foreach ($values as $date => $count) {
|
foreach ($values as $date => $count) {
|
||||||
if ((isset($submissions[$date]) && $submissions[$date] != $count) || (!isset($submissions[$date]) && $count != 0)) {
|
if ((isset($submissions[$date]) && $submissions[$date] != $count) || (!isset($submissions[$date]) && $count != 0)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\UserInvite;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
it('can register with invite token', function () {
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$email = 'invitee@gmail.com';
|
||||||
|
$inviteData = ['email' => $email, 'role' => 'user'];
|
||||||
|
$this->postJson(route('open.workspaces.users.add', $workspace->id), $inviteData)
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
expect($workspace->invites()->count())->toBe(1);
|
||||||
|
$userInvite = UserInvite::latest()->first();
|
||||||
|
$token = $userInvite->token;
|
||||||
|
|
||||||
|
$this->postJson('/logout')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Register with token
|
||||||
|
$response = $this->postJson('/register', [
|
||||||
|
'name' => 'Invitee',
|
||||||
|
'email' => $email,
|
||||||
|
'hear_about_us' => 'google',
|
||||||
|
'password' => 'secret',
|
||||||
|
'password_confirmation' => 'secret',
|
||||||
|
'agree_terms' => true,
|
||||||
|
'invite_token' => $token,
|
||||||
|
]);
|
||||||
|
$response->assertSuccessful();
|
||||||
|
expect($workspace->users()->count())->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot register with expired invite token', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$email = 'invitee@gmail.com';
|
||||||
|
$inviteData = ['email' => $email, 'role' => 'user'];
|
||||||
|
$this->postJson(route('open.workspaces.users.add', $workspace->id), $inviteData)
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
expect($workspace->invites()->count())->toBe(1);
|
||||||
|
$userInvite = UserInvite::latest()->first();
|
||||||
|
$token = $userInvite->token;
|
||||||
|
|
||||||
|
$this->postJson('/logout')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
Carbon::setTestNow(now()->addDays(8));
|
||||||
|
// Register with token
|
||||||
|
$response = $this->postJson('/register', [
|
||||||
|
'name' => 'Invitee',
|
||||||
|
'email' => $email,
|
||||||
|
'hear_about_us' => 'google',
|
||||||
|
'password' => 'secret',
|
||||||
|
'password_confirmation' => 'secret',
|
||||||
|
'agree_terms' => true,
|
||||||
|
'invite_token' => $token,
|
||||||
|
]);
|
||||||
|
$response->assertStatus(400)->assertJson([
|
||||||
|
'message' => 'Invite token has expired.',
|
||||||
|
]);
|
||||||
|
expect($workspace->users()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot re-register with accepted invite token', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$email = 'invitee@gmail.com';
|
||||||
|
$inviteData = ['email' => $email, 'role' => 'user'];
|
||||||
|
$this->postJson(route('open.workspaces.users.add', $workspace->id), $inviteData)
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
expect($workspace->invites()->count())->toBe(1);
|
||||||
|
$userInvite = UserInvite::latest()->first();
|
||||||
|
$token = $userInvite->token;
|
||||||
|
|
||||||
|
$this->postJson('/logout')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Register with token
|
||||||
|
$response = $this->postJson('/register', [
|
||||||
|
'name' => 'Invitee',
|
||||||
|
'email' => $email,
|
||||||
|
'hear_about_us' => 'google',
|
||||||
|
'password' => 'secret',
|
||||||
|
'password_confirmation' => 'secret',
|
||||||
|
'agree_terms' => true,
|
||||||
|
'invite_token' => $token,
|
||||||
|
]);
|
||||||
|
$response->assertSuccessful();
|
||||||
|
expect($workspace->users()->count())->toBe(2);
|
||||||
|
|
||||||
|
$this->postJson('/logout')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Register again with same used token
|
||||||
|
$response = $this->postJson('/register', [
|
||||||
|
'name' => 'Invitee',
|
||||||
|
'email' => $email,
|
||||||
|
'hear_about_us' => 'google',
|
||||||
|
'password' => 'secret',
|
||||||
|
'password_confirmation' => 'secret',
|
||||||
|
'agree_terms' => true,
|
||||||
|
'invite_token' => $token,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)->assertJson([
|
||||||
|
'message' => 'The email has already been taken.',
|
||||||
|
]);
|
||||||
|
expect($workspace->users()->count())->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can cancel user invite', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$email = 'invitee@gmail.com';
|
||||||
|
$inviteData = ['email' => $email, 'role' => 'user'];
|
||||||
|
$response = $this->postJson(route('open.workspaces.users.add', $workspace->id), $inviteData)
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
expect($workspace->invites()->count())->toBe(1);
|
||||||
|
$userInvite = UserInvite::latest()->first();
|
||||||
|
$token = $userInvite->token;
|
||||||
|
|
||||||
|
// Cancel the invite
|
||||||
|
$this->deleteJson(route('open.workspaces.invites.cancel', ['workspaceId' => $workspace->id, 'inviteId' => $userInvite->id]))
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->postJson('/logout')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Register with token
|
||||||
|
$response = $this->postJson('/register', [
|
||||||
|
'name' => 'Invitee',
|
||||||
|
'email' => $email,
|
||||||
|
'hear_about_us' => 'google',
|
||||||
|
'password' => 'secret',
|
||||||
|
'password_confirmation' => 'secret',
|
||||||
|
'agree_terms' => true,
|
||||||
|
'invite_token' => $token,
|
||||||
|
]);
|
||||||
|
$response->assertStatus(400)->assertJson([
|
||||||
|
'message' => 'Invite token is invalid.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($workspace->users()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use App\Mail\UserInvitationEmail;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->user = $this->actingAsProUser();
|
||||||
|
$this->workspace = Workspace::factory()->create();
|
||||||
|
$this->workspace->users()->attach($this->user, ['role' => 'admin']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can list users in a workspace', function () {
|
||||||
|
|
||||||
|
$this->getJson(route('open.workspaces.users.index', ['workspaceId' => $this->workspace->id]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJsonCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add a user to a workspace', function () {
|
||||||
|
$newUser = User::factory()->create(['email' => 'newuser@example.com']);
|
||||||
|
$this->postJson(route('open.workspaces.users.add', ['workspaceId' => $this->workspace->id]), [
|
||||||
|
'email' => $newUser->email,
|
||||||
|
'role' => 'user',
|
||||||
|
])
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'User has been successfully added to workspace.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->workspace->users()->count())->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can send an invitation email to a non-existing user', function () {
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$this->postJson(route('open.workspaces.users.add', ['workspaceId' => $this->workspace->id]), [
|
||||||
|
'email' => 'nonexisting@example.com',
|
||||||
|
'role' => 'user',
|
||||||
|
])
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'Registration invitation email sent to user.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Mail::assertQueued(UserInvitationEmail::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can update user role in a workspace', function () {
|
||||||
|
$existingUser = User::factory()->create();
|
||||||
|
$this->workspace->users()->attach($existingUser, ['role' => 'user']);
|
||||||
|
|
||||||
|
$this->putJson(route('open.workspaces.users.update-role', [
|
||||||
|
'workspaceId' => $this->workspace->id,
|
||||||
|
'userId' => $existingUser->id
|
||||||
|
]), [
|
||||||
|
'role' => 'admin',
|
||||||
|
])
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'User role changed successfully.'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can remove a user from a workspace', function () {
|
||||||
|
$existingUser = User::factory()->create();
|
||||||
|
$this->workspace->users()->attach($existingUser);
|
||||||
|
|
||||||
|
$this->deleteJson(route('open.workspaces.users.remove', [
|
||||||
|
'workspaceId' => $this->workspace->id,
|
||||||
|
'userId' => $existingUser->id
|
||||||
|
]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'User removed from workspace successfully.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->workspace->users()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can leave a workspace', function () {
|
||||||
|
$this->postJson(route('open.workspaces.leave', ['workspaceId' => $this->workspace->id]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'You have left the workspace successfully.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->workspace->users()->count())->toBe(0);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue