Skip to content

Commit 350107d

Browse files
aaryan610viharprateekshourya29NarayanBavisetti
authored
[WEB-5088] feat: Power K v2 (#7905)
* feat: add project shortcut in command palette * feat: global project switcher shortcut * refactor: generalize command palette entity handling * feat: extend command palette navigation * feat: add issue shortcut to command palette * feat: add modular project selection for cycle navigation * chore: add reusable command palette utilities * fix: update key sequence handling to use window methods for timeout management * fix: build errors * chore: minor ux copy improvements * feat: implement a new command registry and renderer for enhanced command palette functionality * feat: introduce new command palette components and enhance search functionality * feat: enhance command palette components with improved initialization and loading indicators * feat: Implement new command palette architecture with multi-step commands, context-aware filtering, and reusable components. Add comprehensive documentation and integration guides. Enhance command execution with a dedicated executor and context provider. Introduce new command types and improve existing command definitions for better usability and maintainability. * refactor: hook arguments * refactor: folder structure * refactor: update import paths * fix: context prop drilling * refactor: update search components * refactor: create actions * chore: add type to pages * chore: init contextual actions * refactor: context based actions code split * chore: module context-based actions * refactor: streamline command execution flow and enhance multi-step handling in command palette * refactor: remove placeholder management from command execution and implement centralized placeholder mapping * chore: cycle context based actions * refactor: simplify command execution by consolidating selection steps and adding page change handling * chore: added more options to work item contextual actions * chore: page context actions * refactor: update step type definitions and enhance page mapping for command execution * feat: implement Command Palette V2 with global shortcuts and enhanced context handling * refactor: power k v2 * refactor: creation commands * feat: add navigation utility for Power K context handling * feat: implement comprehensive navigation commands for Power K * refactor: work item contextual actions * fix: build errors * refactor: remaining contextual actions * refactor: remove old code * chore: update placeholder * refactor: enhance command registry with observable properties and context-aware shortcut handling * refactor: improve command filtering logic in CommandPaletteModal * chore: context indicator * chore: misc actions * style: shortcut badge * feat: add open entity actions and enhance navigation commands for Power K * refactor: rename and reorganize Power K components for improved clarity and structure * refactor: update CommandPalette components and streamline global shortcuts handling * refactor: adjust debounce timing in CommandPaletteModal for improved responsiveness * feat: implement shortcuts modal and enhance command registry for better shortcut management * fix: search implemented * refactor: search results code split * refactor: search results code split * feat: introduce creation and navigation command modules for Power K, enhancing command organization and functionality * chore: update menu logos * refactor: remove unused PowerKOpenEntityActionsExtended component from command palette * refactor: search menu * fix: clear context on backspace and manual clear * refactor: rename creation command keys for consistency and clarity in Power K * chore: added intake in global search * chore: preferences menu * chore: removed the empty serach params * revert: command palette changes * cleanup * refactor: update command IDs to use underscores for consistency across Power K components * refactor: extended context based actions * chore: modal command item status props * refactor: replace CommandPalette with CommandPaletteProvider in settings and profile layouts * refactor: update settings menu to use translated labels instead of i18n labels * refactor: update command titles to use translation keys for creation actions * refactor: update navigation command titles to use translation keys for consistency * chore: minor cleanup * chore: misc commands added * chore: code split for no search results command * chore: state menu items for work item context based commands * chore: add more props to no search results command * chore: add more props to no search results command * refactor: remove shortcut key for create workspace command * Refactor command palette to use PowerK store - Replaced instances of `useCommandPalette` with `usePowerK` across various components, including `AppSearch`, `CommandModal`, and `CommandPalette`. - Introduced `PowerKStore` to manage modal states and commands, enhancing the command palette functionality. - Updated modal handling to toggle `PowerKModal` and `ShortcutsListModal` instead of the previous command palette modals. - Refactored related components to ensure compatibility with the new store structure and maintain functionality. * Refactor PowerK command handling to remove context dependency - Updated `usePowerKCommands` and `usePowerKCreationCommands` to eliminate the need for a context parameter, simplifying their usage. - Adjusted related command records to utilize the new structure, ensuring consistent access to command configurations. - Enhanced permission checks in creation commands to utilize user project roles for better access control. * chore: add context indicator * chore: update type import * chore: migrate toast implementation from @plane/ui to @plane/propel/toast across multiple command files * refactor: power k modal wrapper and provider * fix: type imports * chore: update creation command shortcuts * fix: page context commands * chore: update navigation and open command shortcuts * fix: work item standalone page modals * fix: context indicator visibility * fix: potential error points * fix: build errors * fix: lint errors * fix: import order --------- Co-authored-by: Vihar Kurama <[email protected]> Co-authored-by: Prateek Shourya <[email protected]> Co-authored-by: NarayanBavisetti <[email protected]>
1 parent 73e0e8d commit 350107d

File tree

126 files changed

+5944
-1784
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+5944
-1784
lines changed

apps/api/plane/app/views/search/base.py

Lines changed: 150 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,25 @@ class GlobalSearchEndpoint(BaseAPIView):
4343
also show related workspace if found
4444
"""
4545

46-
def filter_workspaces(self, query, slug, project_id, workspace_search):
46+
def filter_workspaces(self, query, _slug, _project_id, _workspace_search):
4747
fields = ["name"]
4848
q = Q()
49-
for field in fields:
50-
q |= Q(**{f"{field}__icontains": query})
49+
if query:
50+
for field in fields:
51+
q |= Q(**{f"{field}__icontains": query})
5152
return (
5253
Workspace.objects.filter(q, workspace_member__member=self.request.user)
54+
.order_by("-created_at")
5355
.distinct()
5456
.values("name", "id", "slug")
5557
)
5658

57-
def filter_projects(self, query, slug, project_id, workspace_search):
59+
def filter_projects(self, query, slug, _project_id, _workspace_search):
5860
fields = ["name", "identifier"]
5961
q = Q()
60-
for field in fields:
61-
q |= Q(**{f"{field}__icontains": query})
62+
if query:
63+
for field in fields:
64+
q |= Q(**{f"{field}__icontains": query})
6265
return (
6366
Project.objects.filter(
6467
q,
@@ -67,21 +70,23 @@ def filter_projects(self, query, slug, project_id, workspace_search):
6770
archived_at__isnull=True,
6871
workspace__slug=slug,
6972
)
73+
.order_by("-created_at")
7074
.distinct()
7175
.values("name", "id", "identifier", "workspace__slug")
7276
)
7377

7478
def filter_issues(self, query, slug, project_id, workspace_search):
7579
fields = ["name", "sequence_id", "project__identifier"]
7680
q = Q()
77-
for field in fields:
78-
if field == "sequence_id":
79-
# Match whole integers only (exclude decimal numbers)
80-
sequences = re.findall(r"\b\d+\b", query)
81-
for sequence_id in sequences:
82-
q |= Q(**{"sequence_id": sequence_id})
83-
else:
84-
q |= Q(**{f"{field}__icontains": query})
81+
if query:
82+
for field in fields:
83+
if field == "sequence_id":
84+
# Match whole integers only (exclude decimal numbers)
85+
sequences = re.findall(r"\b\d+\b", query)
86+
for sequence_id in sequences:
87+
q |= Q(**{"sequence_id": sequence_id})
88+
else:
89+
q |= Q(**{f"{field}__icontains": query})
8590

8691
issues = Issue.issue_objects.filter(
8792
q,
@@ -106,8 +111,9 @@ def filter_issues(self, query, slug, project_id, workspace_search):
106111
def filter_cycles(self, query, slug, project_id, workspace_search):
107112
fields = ["name"]
108113
q = Q()
109-
for field in fields:
110-
q |= Q(**{f"{field}__icontains": query})
114+
if query:
115+
for field in fields:
116+
q |= Q(**{f"{field}__icontains": query})
111117

112118
cycles = Cycle.objects.filter(
113119
q,
@@ -120,13 +126,20 @@ def filter_cycles(self, query, slug, project_id, workspace_search):
120126
if workspace_search == "false" and project_id:
121127
cycles = cycles.filter(project_id=project_id)
122128

123-
return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
129+
return (
130+
cycles.order_by("-created_at")
131+
.distinct()
132+
.values(
133+
"name", "id", "project_id", "project__identifier", "workspace__slug"
134+
)
135+
)
124136

125137
def filter_modules(self, query, slug, project_id, workspace_search):
126138
fields = ["name"]
127139
q = Q()
128-
for field in fields:
129-
q |= Q(**{f"{field}__icontains": query})
140+
if query:
141+
for field in fields:
142+
q |= Q(**{f"{field}__icontains": query})
130143

131144
modules = Module.objects.filter(
132145
q,
@@ -139,13 +152,20 @@ def filter_modules(self, query, slug, project_id, workspace_search):
139152
if workspace_search == "false" and project_id:
140153
modules = modules.filter(project_id=project_id)
141154

142-
return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
155+
return (
156+
modules.order_by("-created_at")
157+
.distinct()
158+
.values(
159+
"name", "id", "project_id", "project__identifier", "workspace__slug"
160+
)
161+
)
143162

144163
def filter_pages(self, query, slug, project_id, workspace_search):
145164
fields = ["name"]
146165
q = Q()
147-
for field in fields:
148-
q |= Q(**{f"{field}__icontains": query})
166+
if query:
167+
for field in fields:
168+
q |= Q(**{f"{field}__icontains": query})
149169

150170
pages = (
151171
Page.objects.filter(
@@ -157,7 +177,9 @@ def filter_pages(self, query, slug, project_id, workspace_search):
157177
)
158178
.annotate(
159179
project_ids=Coalesce(
160-
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
180+
ArrayAgg(
181+
"projects__id", distinct=True, filter=~Q(projects__id=True)
182+
),
161183
Value([], output_field=ArrayField(UUIDField())),
162184
)
163185
)
@@ -174,19 +196,28 @@ def filter_pages(self, query, slug, project_id, workspace_search):
174196
)
175197

176198
if workspace_search == "false" and project_id:
177-
project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list(
178-
"project_id", flat=True
179-
)[:1]
199+
project_subquery = ProjectPage.objects.filter(
200+
page_id=OuterRef("id"), project_id=project_id
201+
).values_list("project_id", flat=True)[:1]
180202

181-
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id)
203+
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(
204+
project_id=project_id
205+
)
182206

183-
return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug")
207+
return (
208+
pages.order_by("-created_at")
209+
.distinct()
210+
.values(
211+
"name", "id", "project_ids", "project_identifiers", "workspace__slug"
212+
)
213+
)
184214

185215
def filter_views(self, query, slug, project_id, workspace_search):
186216
fields = ["name"]
187217
q = Q()
188-
for field in fields:
189-
q |= Q(**{f"{field}__icontains": query})
218+
if query:
219+
for field in fields:
220+
q |= Q(**{f"{field}__icontains": query})
190221

191222
issue_views = IssueView.objects.filter(
192223
q,
@@ -199,29 +230,57 @@ def filter_views(self, query, slug, project_id, workspace_search):
199230
if workspace_search == "false" and project_id:
200231
issue_views = issue_views.filter(project_id=project_id)
201232

202-
return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
233+
return (
234+
issue_views.order_by("-created_at")
235+
.distinct()
236+
.values(
237+
"name", "id", "project_id", "project__identifier", "workspace__slug"
238+
)
239+
)
240+
241+
def filter_intakes(self, query, slug, project_id, workspace_search):
242+
fields = ["name", "sequence_id", "project__identifier"]
243+
q = Q()
244+
if query:
245+
for field in fields:
246+
if field == "sequence_id":
247+
# Match whole integers only (exclude decimal numbers)
248+
sequences = re.findall(r"\b\d+\b", query)
249+
for sequence_id in sequences:
250+
q |= Q(**{"sequence_id": sequence_id})
251+
else:
252+
q |= Q(**{f"{field}__icontains": query})
253+
254+
issues = Issue.objects.filter(
255+
q,
256+
project__project_projectmember__member=self.request.user,
257+
project__project_projectmember__is_active=True,
258+
project__archived_at__isnull=True,
259+
workspace__slug=slug,
260+
).filter(models.Q(issue_intake__status=0) | models.Q(issue_intake__status=-2))
261+
262+
if workspace_search == "false" and project_id:
263+
issues = issues.filter(project_id=project_id)
264+
265+
return (
266+
issues.order_by("-created_at")
267+
.distinct()
268+
.values(
269+
"name",
270+
"id",
271+
"sequence_id",
272+
"project__identifier",
273+
"project_id",
274+
"workspace__slug",
275+
)[:100]
276+
)
203277

204278
def get(self, request, slug):
205279
query = request.query_params.get("search", False)
280+
entities_param = request.query_params.get("entities")
206281
workspace_search = request.query_params.get("workspace_search", "false")
207282
project_id = request.query_params.get("project_id", False)
208283

209-
if not query:
210-
return Response(
211-
{
212-
"results": {
213-
"workspace": [],
214-
"project": [],
215-
"issue": [],
216-
"cycle": [],
217-
"module": [],
218-
"issue_view": [],
219-
"page": [],
220-
}
221-
},
222-
status=status.HTTP_200_OK,
223-
)
224-
225284
MODELS_MAPPER = {
226285
"workspace": self.filter_workspaces,
227286
"project": self.filter_projects,
@@ -230,13 +289,27 @@ def get(self, request, slug):
230289
"module": self.filter_modules,
231290
"issue_view": self.filter_views,
232291
"page": self.filter_pages,
292+
"intake": self.filter_intakes,
233293
}
234294

295+
# Determine which entities to search
296+
if entities_param:
297+
requested_entities = [
298+
e.strip() for e in entities_param.split(",") if e.strip()
299+
]
300+
requested_entities = [e for e in requested_entities if e in MODELS_MAPPER]
301+
else:
302+
requested_entities = list(MODELS_MAPPER.keys())
303+
235304
results = {}
236305

237-
for model in MODELS_MAPPER.keys():
238-
func = MODELS_MAPPER.get(model, None)
239-
results[model] = func(query, slug, project_id, workspace_search)
306+
for entity in requested_entities:
307+
func = MODELS_MAPPER.get(entity)
308+
if func:
309+
results[entity] = func(
310+
query or None, slug, project_id, workspace_search
311+
)
312+
240313
return Response({"results": results}, status=status.HTTP_200_OK)
241314

242315

@@ -316,12 +389,15 @@ def get(self, request, slug):
316389
projects = (
317390
Project.objects.filter(
318391
q,
319-
Q(project_projectmember__member=self.request.user) | Q(network=2),
392+
Q(project_projectmember__member=self.request.user)
393+
| Q(network=2),
320394
workspace__slug=slug,
321395
)
322396
.order_by("-created_at")
323397
.distinct()
324-
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
398+
.values(
399+
"name", "id", "identifier", "logo_props", "workspace__slug"
400+
)[:count]
325401
)
326402
response_data["project"] = list(projects)
327403

@@ -380,16 +456,20 @@ def get(self, request, slug):
380456
.annotate(
381457
status=Case(
382458
When(
383-
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
459+
Q(start_date__lte=timezone.now())
460+
& Q(end_date__gte=timezone.now()),
384461
then=Value("CURRENT"),
385462
),
386463
When(
387464
start_date__gt=timezone.now(),
388465
then=Value("UPCOMING"),
389466
),
390-
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
391467
When(
392-
Q(start_date__isnull=True) & Q(end_date__isnull=True),
468+
end_date__lt=timezone.now(), then=Value("COMPLETED")
469+
),
470+
When(
471+
Q(start_date__isnull=True)
472+
& Q(end_date__isnull=True),
393473
then=Value("DRAFT"),
394474
),
395475
default=Value("DRAFT"),
@@ -507,7 +587,9 @@ def get(self, request, slug):
507587
)
508588
)
509589
.order_by("-created_at")
510-
.values("member__avatar_url", "member__display_name", "member__id")[:count]
590+
.values(
591+
"member__avatar_url", "member__display_name", "member__id"
592+
)[:count]
511593
)
512594
response_data["user_mention"] = list(users)
513595

@@ -521,12 +603,15 @@ def get(self, request, slug):
521603
projects = (
522604
Project.objects.filter(
523605
q,
524-
Q(project_projectmember__member=self.request.user) | Q(network=2),
606+
Q(project_projectmember__member=self.request.user)
607+
| Q(network=2),
525608
workspace__slug=slug,
526609
)
527610
.order_by("-created_at")
528611
.distinct()
529-
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
612+
.values(
613+
"name", "id", "identifier", "logo_props", "workspace__slug"
614+
)[:count]
530615
)
531616
response_data["project"] = list(projects)
532617

@@ -583,16 +668,20 @@ def get(self, request, slug):
583668
.annotate(
584669
status=Case(
585670
When(
586-
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
671+
Q(start_date__lte=timezone.now())
672+
& Q(end_date__gte=timezone.now()),
587673
then=Value("CURRENT"),
588674
),
589675
When(
590676
start_date__gt=timezone.now(),
591677
then=Value("UPCOMING"),
592678
),
593-
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
594679
When(
595-
Q(start_date__isnull=True) & Q(end_date__isnull=True),
680+
end_date__lt=timezone.now(), then=Value("COMPLETED")
681+
),
682+
When(
683+
Q(start_date__isnull=True)
684+
& Q(end_date__isnull=True),
596685
then=Value("DRAFT"),
597686
),
598687
default=Value("DRAFT"),

0 commit comments

Comments
 (0)