|
1 | 1 | <script lang="ts"> |
2 | 2 | import { Wizard } from '$lib/layout'; |
3 | | - import { Icon, Layout, Tag, Typography, Button, Card } from '@appwrite.io/pink-svelte'; |
| 3 | + import { Icon, Input, Layout, Popover, Tag, Typography, Card } from '@appwrite.io/pink-svelte'; |
4 | 4 | import { supportData, isSupportOnline } from './wizard/support/store'; |
5 | | - import { onMount } from 'svelte'; |
| 5 | + import { onMount, onDestroy } from 'svelte'; |
6 | 6 | import { sdk } from '$lib/stores/sdk'; |
7 | | - import { Form, InputSelect, InputText, InputTextarea } from '$lib/elements/forms/index.js'; |
8 | | -
|
| 7 | + import { |
| 8 | + Form, |
| 9 | + InputSelect, |
| 10 | + InputText, |
| 11 | + InputTextarea, |
| 12 | + Button |
| 13 | + } from '$lib/elements/forms/index.js'; |
| 14 | + import { Query } from '@appwrite.io/console'; |
9 | 15 | import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; |
10 | 16 | import { |
11 | 17 | localeTimezoneName, |
|
18 | 24 | import { user } from '$lib/stores/user'; |
19 | 25 | import { wizard } from '$lib/stores/wizard'; |
20 | 26 | import { VARS } from '$lib/system'; |
21 | | - import { onDestroy } from 'svelte'; |
22 | | - import { IconCheckCircle, IconXCircle } from '@appwrite.io/pink-icons-svelte'; |
| 27 | + import { IconCheckCircle, IconXCircle, IconInfo } from '@appwrite.io/pink-icons-svelte'; |
| 28 | +
|
| 29 | + let projectOptions = $state<Array<{ value: string; label: string }>>([]); |
23 | 30 |
|
24 | | - let projectOptions: Array<{ value: string; label: string }>; |
| 31 | + // Category options with display names |
| 32 | + const categories = [ |
| 33 | + { value: 'general', label: 'General' }, |
| 34 | + { value: 'billing', label: 'Billing' }, |
| 35 | + { value: 'technical', label: 'Technical' } |
| 36 | + ]; |
| 37 | +
|
| 38 | + // Topic options based on category |
| 39 | + const topicsByCategory = { |
| 40 | + general: ['Security', 'Compliance', 'Performance'], |
| 41 | + billing: ['Invoices', 'Plans'], |
| 42 | + technical: [ |
| 43 | + 'Auth', |
| 44 | + 'Databases', |
| 45 | + 'Storage', |
| 46 | + 'Functions', |
| 47 | + 'Realtime', |
| 48 | + 'Messaging', |
| 49 | + 'Migrations', |
| 50 | + 'Webhooks', |
| 51 | + 'SDKs', |
| 52 | + 'Console' |
| 53 | + ] |
| 54 | + }; |
| 55 | +
|
| 56 | + // Severity options |
| 57 | + const severityOptions = [ |
| 58 | + { value: 'critical', label: 'Critical' }, |
| 59 | + { value: 'high', label: 'High' }, |
| 60 | + { value: 'medium', label: 'Medium' }, |
| 61 | + { value: 'low', label: 'Low' }, |
| 62 | + { value: 'question', label: 'Question' } |
| 63 | + ]; |
25 | 64 |
|
26 | 65 | onMount(async () => { |
27 | | - const projectList = await sdk.forConsole.projects.list(); |
| 66 | + // Filter projects by organization ID using server-side queries |
| 67 | + const projectList = await sdk.forConsole.projects.list({ |
| 68 | + queries: $organization?.$id ? [Query.equal('teamId', $organization.$id)] : [] |
| 69 | + }); |
28 | 70 | projectOptions = projectList.projects.map((project) => ({ |
29 | 71 | value: project.$id, |
30 | 72 | label: project.name |
31 | 73 | })); |
32 | 74 | }); |
33 | 75 |
|
| 76 | + // Cleanup on component destroy |
34 | 77 | onDestroy(() => { |
35 | 78 | $supportData = { |
36 | 79 | message: null, |
37 | 80 | subject: null, |
38 | | - category: 'general', |
| 81 | + category: 'technical', |
| 82 | + topic: undefined, |
| 83 | + severity: 'question', |
39 | 84 | file: null |
40 | 85 | }; |
41 | 86 | }); |
42 | 87 |
|
| 88 | + // Update topic options when category changes |
| 89 | + const topicOptions = $derived( |
| 90 | + ($supportData.category ? topicsByCategory[$supportData.category] || [] : []).map( |
| 91 | + (topic) => ({ |
| 92 | + value: topic.toLowerCase(), |
| 93 | + label: topic |
| 94 | + }) |
| 95 | + ) |
| 96 | + ); |
| 97 | +
|
43 | 98 | async function handleSubmit() { |
| 99 | + // Create category-topic tag |
| 100 | + const categoryTopicTag = $supportData.topic |
| 101 | + ? `${$supportData.category}-${$supportData.topic}`.toLowerCase() |
| 102 | + : $supportData.category.toLowerCase(); |
| 103 | +
|
44 | 104 | const response = await fetch(`${VARS.GROWTH_ENDPOINT}/support`, { |
45 | 105 | method: 'POST', |
46 | 106 | headers: { |
|
51 | 111 | subject: $supportData.subject, |
52 | 112 | firstName: ($user?.name || 'Unknown').slice(0, 40), |
53 | 113 | message: $supportData.message, |
54 | | - tags: ['cloud'], |
| 114 | + tags: [categoryTopicTag], |
55 | 115 | customFields: [ |
56 | 116 | { id: '41612', value: $supportData.category }, |
57 | | - { id: '48493', value: $user?.name ?? '' }, |
58 | 117 | { id: '48492', value: $organization?.$id ?? '' }, |
59 | 118 | { id: '48491', value: $supportData?.project ?? '' }, |
60 | | - { id: '48490', value: $user?.$id ?? '' } |
| 119 | + { id: '56023', value: $supportData?.severity ?? '' }, |
| 120 | + { id: '56024', value: $organization?.billingPlan ?? '' } |
61 | 121 | ] |
62 | 122 | }) |
63 | 123 | }); |
|
84 | 144 | $supportData = { |
85 | 145 | message: null, |
86 | 146 | subject: null, |
87 | | - category: 'general', |
| 147 | + category: 'technical', |
| 148 | + topic: undefined, |
| 149 | + severity: undefined, |
88 | 150 | file: null, |
89 | 151 | project: null |
90 | 152 | }; |
|
99 | 161 | endDay: 'Friday' as WeekDay |
100 | 162 | }; |
101 | 163 |
|
102 | | - $: supportTimings = `${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`; |
103 | | - $: supportWeekDays = `${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`; |
| 164 | + const supportTimings = $derived( |
| 165 | + `${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}` |
| 166 | + ); |
| 167 | + const supportWeekDays = $derived( |
| 168 | + `${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}` |
| 169 | + ); |
104 | 170 | </script> |
105 | 171 |
|
| 172 | +{#snippet severityPopover()} |
| 173 | + <Popover let:toggle> |
| 174 | + <Button extraCompact size="s" on:click={toggle}> |
| 175 | + <Icon size="s" icon={IconInfo} /> |
| 176 | + </Button> |
| 177 | + <div slot="tooltip" style="max-width: 400px;"> |
| 178 | + <Layout.Stack gap="s"> |
| 179 | + <Typography.Text> |
| 180 | + <b>Critical:</b> System is down or a critical component is non-functional, causing |
| 181 | + a complete stoppage of work or significant business impact. |
| 182 | + </Typography.Text> |
| 183 | + <Typography.Text> |
| 184 | + <b>High:</b> Major functionality is impaired, but a workaround is available, or a |
| 185 | + critical component is significantly degraded. |
| 186 | + </Typography.Text> |
| 187 | + <Typography.Text> |
| 188 | + <b>Medium:</b> Minor functionality is impaired without significant business impact. |
| 189 | + </Typography.Text> |
| 190 | + <Typography.Text> |
| 191 | + <b>Low:</b> Issue has minor impact on business operations; workaround is not necessary. |
| 192 | + </Typography.Text> |
| 193 | + <Typography.Text> |
| 194 | + <b>Question:</b> Requests for information, general guidance, or feature requests. |
| 195 | + </Typography.Text> |
| 196 | + </Layout.Stack> |
| 197 | + </div> |
| 198 | + </Popover> |
| 199 | +{/snippet} |
| 200 | + |
106 | 201 | <Wizard title="Contact us" confirmExit={true}> |
107 | 202 | <Form onSubmit={handleSubmit}> |
108 | 203 | <Layout.Stack gap="xl"> |
|
113 | 208 | </Layout.Stack> |
114 | 209 | <Layout.Stack gap="s"> |
115 | 210 | <Typography.Text color="--fgcolor-neutral-secondary" |
116 | | - >Choose a topic</Typography.Text> |
| 211 | + >Choose a category</Typography.Text> |
117 | 212 | <Layout.Stack gap="s" direction="row"> |
118 | | - {#each ['general', 'billing', 'technical'] as category} |
| 213 | + {#each categories as category} |
119 | 214 | <Tag |
120 | 215 | on:click={() => { |
121 | | - $supportData.category = category; |
| 216 | + if ($supportData.category !== category.value) { |
| 217 | + $supportData.topic = undefined; |
| 218 | + } |
| 219 | + $supportData.category = category.value; |
122 | 220 | }} |
123 | | - selected={$supportData.category === category}>{category}</Tag> |
| 221 | + selected={$supportData.category === category.value} |
| 222 | + >{category.label}</Tag> |
124 | 223 | {/each} |
125 | 224 | </Layout.Stack> |
126 | 225 | </Layout.Stack> |
127 | | - <InputSelect |
128 | | - required |
| 226 | + {#if topicOptions.length > 0} |
| 227 | + {#key $supportData.category} |
| 228 | + <Input.ComboBox |
| 229 | + id="topic" |
| 230 | + label="Choose a topic" |
| 231 | + placeholder="Select topic" |
| 232 | + bind:value={$supportData.topic} |
| 233 | + options={topicOptions} /> |
| 234 | + {/key} |
| 235 | + {/if} |
| 236 | + <Input.ComboBox |
129 | 237 | id="project" |
130 | 238 | label="Choose a project" |
131 | 239 | options={projectOptions ?? []} |
132 | 240 | bind:value={$supportData.project} |
133 | 241 | placeholder="Select project" /> |
| 242 | + <InputSelect |
| 243 | + id="severity" |
| 244 | + label="Severity" |
| 245 | + options={severityOptions} |
| 246 | + bind:value={$supportData.severity} |
| 247 | + required |
| 248 | + placeholder="Select severity"> |
| 249 | + <div slot="info"> |
| 250 | + {@render severityPopover()} |
| 251 | + </div> |
| 252 | + </InputSelect> |
134 | 253 | <InputText |
135 | 254 | id="subject" |
136 | 255 | label="Subject" |
|
143 | 262 | bind:value={$supportData.message} |
144 | 263 | placeholder="Type here..." |
145 | 264 | label="Tell us a bit more" |
| 265 | + required |
146 | 266 | maxlength={4096} /> |
147 | 267 | <Layout.Stack direction="row" justifyContent="flex-end" gap="s"> |
148 | | - <Button.Button |
| 268 | + <Button |
149 | 269 | size="s" |
150 | | - variant="secondary" |
| 270 | + secondary |
151 | 271 | on:click={() => { |
152 | 272 | wizard.hide(); |
153 | | - }}>Cancel</Button.Button> |
154 | | - <Button.Button size="s">Submit</Button.Button> |
| 273 | + }}>Cancel</Button> |
| 274 | + <Button submit size="s">Submit</Button> |
155 | 275 | </Layout.Stack> |
156 | 276 | </Layout.Stack> |
157 | 277 | </Form> |
|
0 commit comments