You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/05/28 07:16:30 UTC

(texera) branch main updated (d8c254c75a -> 7bd6550056)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


    from d8c254c75a test: add unit spec for UserDatasetComponent (#5238)
     new a4a3df5af3 test: unit tests for LandingPageComponent (#5234)
     new 7bd6550056 feat: Welcome first-time contributors with comment commands guide (#5222)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../workflows/welcome-first-time-contributor.yml   | 157 +++++++++++++++
 CONTRIBUTING.md                                    |  40 ++++
 .../landing-page/landing-page.component.spec.ts    | 213 +++++++++++++++++++++
 3 files changed, 410 insertions(+)
 create mode 100644 .github/workflows/welcome-first-time-contributor.yml
 create mode 100644 frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts


(texera) 01/02: test: unit tests for LandingPageComponent (#5234)

Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git

commit a4a3df5af33ef4b67ca5ebb3d4be52efd00d4254
Author: Matthew B. <mg...@uci.edu>
AuthorDate: Wed May 27 23:47:49 2026 -0700

    test: unit tests for LandingPageComponent (#5234)
    
    ### What changes were proposed in this PR?
    - Add `landing-page.component.spec.ts` covering creation, the
    constructor's `userChanged()` subscription (login/logout transitions
    update `isLogin` and `currentUid`), `ngOnInit` calling both
    `getWorkflowCount` and `loadTops`, count population from
    `HubService.getCount`, the three top-loved buckets populated by
    `loadTops`, the `loadTops` error path logging via `console.error`,
    `getTopLovedEntries` enriching items via
    `SearchService.extendSearchResultsWithHubActivityInfo(items, true,
    ["access"])` per action and returning a map keyed by action, and
    `navigateToSearch` routing for `"workflow"`, `"dataset"`, and unknown
    types.
    - Stub `HubService`, `SearchService`, `WorkflowPersistService`, and
    `DatasetService`; reuse `StubUserService` for a real `userChanged()`
    subject; replace `Router.navigate` with a vitest spy.
    ### Any related issues, documentation, or discussions?
    Closes: #5225
    ### How was this PR tested?
    - Ran `yarn ng test --watch=false
    --include='src/app/hub/component/landing-page/landing-page.component.spec.ts'`,
    10/10 tests pass.
    - Ran `yarn format:fix` (1 file formatted, 505 unchanged).
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.7 in compliance with ASF
---
 .../landing-page/landing-page.component.spec.ts    | 213 +++++++++++++++++++++
 1 file changed, 213 insertions(+)

diff --git a/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts
new file mode 100644
index 0000000000..8585ee1c54
--- /dev/null
+++ b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts
@@ -0,0 +1,213 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { Router } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { of, throwError } from "rxjs";
+import { vi } from "vitest";
+
+import { LandingPageComponent } from "./landing-page.component";
+import { ActionType, EntityType, HubService } from "../../service/hub.service";
+import { SearchService } from "../../../dashboard/service/user/search.service";
+import { UserService } from "../../../common/service/user/user.service";
+import { StubUserService } from "../../../common/service/user/stub-user.service";
+import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service";
+import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service";
+import {
+  DASHBOARD_HOME,
+  DASHBOARD_HUB_DATASET_RESULT,
+  DASHBOARD_HUB_WORKFLOW_RESULT,
+} from "../../../app-routing.constant";
+import { commonTestProviders } from "../../../common/testing/test-utils";
+
+describe("LandingPageComponent", () => {
+  let component: LandingPageComponent;
+  let fixture: ComponentFixture<LandingPageComponent>;
+  let hubServiceStub: {
+    getCount: ReturnType<typeof vi.fn>;
+    getTops: ReturnType<typeof vi.fn>;
+  };
+  let searchServiceStub: {
+    extendSearchResultsWithHubActivityInfo: ReturnType<typeof vi.fn>;
+  };
+  let userService: StubUserService;
+  let routerNavigateSpy: ReturnType<typeof vi.fn>;
+
+  // Workflow tops are returned for both Like and Clone; dataset tops for Like only.
+  // Each call to `extendSearchResultsWithHubActivityInfo` is given a tag so the
+  // tests can assert which action bucket each enriched payload landed in.
+  const workflowLikeItems = [{ id: "wf-like-item" }] as any;
+  const workflowCloneItems = [{ id: "wf-clone-item" }] as any;
+  const datasetLikeItems = [{ id: "ds-like-item" }] as any;
+  const workflowLikeEnriched = [{ id: "wf-like-enriched" }] as any;
+  const workflowCloneEnriched = [{ id: "wf-clone-enriched" }] as any;
+  const datasetLikeEnriched = [{ id: "ds-like-enriched" }] as any;
+
+  function configureModule() {
+    hubServiceStub = {
+      getCount: vi.fn((entityType: EntityType) => {
+        if (entityType === EntityType.Workflow) return of(42);
+        if (entityType === EntityType.Dataset) return of(7);
+        return of(0);
+      }),
+      getTops: vi.fn((entityType: EntityType, _actions: ActionType[], _uid?: number) => {
+        if (entityType === EntityType.Workflow) {
+          return of({ [ActionType.Like]: workflowLikeItems, [ActionType.Clone]: workflowCloneItems });
+        }
+        return of({ [ActionType.Like]: datasetLikeItems });
+      }),
+    };
+
+    searchServiceStub = {
+      extendSearchResultsWithHubActivityInfo: vi.fn((items: any[]) => {
+        if (items === workflowLikeItems) return of(workflowLikeEnriched);
+        if (items === workflowCloneItems) return of(workflowCloneEnriched);
+        if (items === datasetLikeItems) return of(datasetLikeEnriched);
+        return of([]);
+      }),
+    };
+
+    TestBed.configureTestingModule({
+      imports: [LandingPageComponent, RouterTestingModule.withRoutes([])],
+      providers: [
+        { provide: HubService, useValue: hubServiceStub },
+        { provide: SearchService, useValue: searchServiceStub },
+        { provide: UserService, useClass: StubUserService },
+        { provide: WorkflowPersistService, useValue: {} },
+        { provide: DatasetService, useValue: {} },
+        ...commonTestProviders,
+      ],
+    });
+
+    userService = TestBed.inject(UserService) as unknown as StubUserService;
+    const router = TestBed.inject(Router);
+    routerNavigateSpy = vi.fn().mockResolvedValue(true);
+    router.navigate = routerNavigateSpy as any;
+  }
+
+  function build() {
+    fixture = TestBed.createComponent(LandingPageComponent);
+    component = fixture.componentInstance;
+  }
+
+  beforeEach(() => {
+    configureModule();
+  });
+
+  it("should create", () => {
+    build();
+    expect(component).toBeTruthy();
+  });
+
+  it("updates isLogin and currentUid when userChanged() emits", () => {
+    build();
+    // Emit a logged-out state.
+    userService.user = undefined;
+    userService.userChangeSubject.next(undefined);
+    expect(component.isLogin).toBe(false);
+    expect(component.currentUid).toBeUndefined();
+
+    // Emit a logged-in state.
+    const newUser = { uid: 99, name: "x", email: "x@x", role: "REGULAR" } as any;
+    userService.user = newUser;
+    userService.userChangeSubject.next(newUser);
+    expect(component.isLogin).toBe(true);
+    expect(component.currentUid).toBe(99);
+  });
+
+  it("ngOnInit invokes getWorkflowCount and loadTops", () => {
+    build();
+    const countSpy = vi.spyOn(component, "getWorkflowCount");
+    const loadSpy = vi.spyOn(component, "loadTops").mockResolvedValue(undefined as any);
+    component.ngOnInit();
+    expect(countSpy).toHaveBeenCalledTimes(1);
+    expect(loadSpy).toHaveBeenCalledTimes(1);
+  });
+
+  it("getWorkflowCount populates workflowCount and datasetCount from HubService.getCount", () => {
+    build();
+    component.getWorkflowCount();
+    expect(hubServiceStub.getCount).toHaveBeenCalledWith(EntityType.Workflow);
+    expect(hubServiceStub.getCount).toHaveBeenCalledWith(EntityType.Dataset);
+    expect(component.workflowCount).toBe(42);
+    expect(component.datasetCount).toBe(7);
+  });
+
+  it("loadTops resolves workflow Like/Clone and dataset Like buckets", async () => {
+    build();
+    await component.loadTops();
+
+    expect(hubServiceStub.getTops).toHaveBeenCalledWith(
+      EntityType.Workflow,
+      [ActionType.Like, ActionType.Clone],
+      component.currentUid
+    );
+    expect(hubServiceStub.getTops).toHaveBeenCalledWith(EntityType.Dataset, [ActionType.Like], component.currentUid);
+
+    expect(component.topLovedWorkflows).toBe(workflowLikeEnriched);
+    expect(component.topClonedWorkflows).toBe(workflowCloneEnriched);
+    expect(component.topLovedDatasets).toBe(datasetLikeEnriched);
+  });
+
+  it("loadTops swallows errors and logs them via console.error", async () => {
+    hubServiceStub.getTops.mockReturnValueOnce(throwError(() => new Error("boom")));
+    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+    build();
+    await component.loadTops();
+    expect(errorSpy).toHaveBeenCalledWith("Failed to load top entries:", expect.any(Error));
+    // Arrays remain at their initial empty state.
+    expect(component.topLovedWorkflows).toEqual([]);
+    expect(component.topClonedWorkflows).toEqual([]);
+    expect(component.topLovedDatasets).toEqual([]);
+    errorSpy.mockRestore();
+  });
+
+  it("getTopLovedEntries extends each action's items with SearchService and returns a map keyed by action", async () => {
+    build();
+    const result = await component.getTopLovedEntries(EntityType.Workflow, [ActionType.Like, ActionType.Clone]);
+
+    expect(searchServiceStub.extendSearchResultsWithHubActivityInfo).toHaveBeenCalledWith(workflowLikeItems, true, [
+      "access",
+    ]);
+    expect(searchServiceStub.extendSearchResultsWithHubActivityInfo).toHaveBeenCalledWith(workflowCloneItems, true, [
+      "access",
+    ]);
+    expect(result[ActionType.Like]).toBe(workflowLikeEnriched);
+    expect(result[ActionType.Clone]).toBe(workflowCloneEnriched);
+  });
+
+  it("navigateToSearch routes to the workflow hub result for 'workflow'", () => {
+    build();
+    component.navigateToSearch("workflow");
+    expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_WORKFLOW_RESULT]);
+  });
+
+  it("navigateToSearch routes to the dataset hub result for 'dataset'", () => {
+    build();
+    component.navigateToSearch("dataset");
+    expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_DATASET_RESULT]);
+  });
+
+  it("navigateToSearch routes to the dashboard home for an unknown type", () => {
+    build();
+    component.navigateToSearch("something-else");
+    expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HOME]);
+  });
+});


(texera) 02/02: feat: Welcome first-time contributors with comment commands guide (#5222)

Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 7bd65500562724eced01d87c254fdab1e9474e7b
Author: Matthew B. <mg...@uci.edu>
AuthorDate: Wed May 27 23:48:09 2026 -0700

    feat: Welcome first-time contributors with comment commands guide (#5222)
    
    ### What changes were proposed in this PR?
    - Add `.github/workflows/welcome-first-time-contributor.yml`, which
    posts a single welcome comment listing the `/take`, `/untake`,
    `/sub-issue`, `/unsub-issue`, `/parent-issue`, `/unparent-issue`,
    `/request-review`, and `/unrequest-review` commands when a new
    contributor opens their first issue or PR.
    - Detect first-time status with the GitHub search API
    (`repo:<owner>/<repo> is:issue|pr author:<login>`, treating `total_count
    <= 1` as first time). This works uniformly for issues and PRs, whereas
    `author_association` reports `NONE` for someone opening their first
    issue, and would miss them.
    - Trigger on `pull_request_target` (not `pull_request`) so PRs from
    forks still receive the welcome with a write-capable token; restrict
    permissions to `issues: write` and `pull-requests: write`.
    - Make the workflow idempotent via a hidden HTML marker (`<!--
    texera:welcome-first-time-contributor -->`) embedded in the welcome body
    and checked against existing comments before posting, so workflow
    re-runs and reopen races do not duplicate the welcome.
      ### Any related issues, documentation, or discussions?
      Closes: #5166
      ### How was this PR tested?
    - Validated the workflow YAML parses cleanly with `python3 -c "import
    yaml;
    yaml.safe_load(open('.github/workflows/welcome-first-time-contributor.yml'))"`.
    - Reviewed logic for the known edge cases: search indexing delay (the
    `<= 1` check tolerates both 0, not yet indexed, and 1, only the new
    item), transient `listComments` failure (falls open rather than dropping
    a genuine first-timer's welcome), and Bot guard at the job-level `if`.
    - Runtime behavior cannot be exercised end-to-end locally; will be
    observable on the next first-time issue or PR after merge.
      ### Was this PR authored or co-authored using generative AI tooling?
      Co-Authored with Claude Opus 4.7 in compliance with ASF Guidelines
---
 .../workflows/welcome-first-time-contributor.yml   | 157 +++++++++++++++++++++
 CONTRIBUTING.md                                    |  40 ++++++
 2 files changed, 197 insertions(+)

diff --git a/.github/workflows/welcome-first-time-contributor.yml b/.github/workflows/welcome-first-time-contributor.yml
new file mode 100644
index 0000000000..df67bebce0
--- /dev/null
+++ b/.github/workflows/welcome-first-time-contributor.yml
@@ -0,0 +1,157 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Welcome first-time contributors when they open an issue or pull
+# request, pointing them at the comment-driven commands defined in
+# `comment-commands.yml` (/take, /request-review, /sub-issue, etc.).
+#
+# Detection uses the search API rather than `author_association`:
+# `author_association` is FIRST_TIME_CONTRIBUTOR only on the first
+# *commit/PR*, so it misses someone opening their first issue (they
+# show up as NONE alongside any non-member who has commented before).
+# Searching `repo:<repo> is:issue author:<login>` with `total_count
+# <= 1` cleanly covers both issues and PRs, tolerating the brief
+# indexing delay where the just-opened item may not be in results yet.
+#
+# Uses `pull_request_target` so PRs from forks still get a welcome
+# comment — `pull_request` from forks runs with a read-only token.
+name: Welcome first-time contributor
+on:
+  issues:
+    types: [opened]
+  pull_request_target:
+    types: [opened]
+
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  welcome:
+    if: github.event.sender.type != 'Bot'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v8
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const isPR = context.eventName === 'pull_request_target';
+            const subject = isPR
+              ? context.payload.pull_request
+              : context.payload.issue;
+            const author = subject.user.login;
+            const issue_number = subject.number;
+            const { owner, repo } = context.repo;
+
+            // Hidden marker for idempotency: if a previous run already
+            // welcomed this issue/PR, the marker will be in an existing
+            // comment and we skip. Lets us survive workflow re-runs,
+            // reopen races, and future manual triggers.
+            const MARKER = '<!-- texera:welcome-first-time-contributor -->';
+            try {
+              const existing = await github.paginate(
+                github.rest.issues.listComments,
+                { owner, repo, issue_number, per_page: 100 },
+              );
+              if (existing.some((c) => (c.body || '').includes(MARKER))) {
+                core.info(`Already welcomed on #${issue_number}; skipping.`);
+                return;
+              }
+            } catch (e) {
+              core.warning(
+                `listComments on #${issue_number} failed: ${e.message}`,
+              );
+              // Fall through — better to risk a duplicate welcome than
+              // skip a genuine first-timer over a transient API error.
+            }
+
+            // Count prior items of the same kind by this author. The
+            // just-opened item may or may not be indexed yet, so we
+            // treat <=1 as "first time" (covers both 0 — not yet
+            // indexed — and 1 — only the new item).
+            const q = `repo:${owner}/${repo} is:${isPR ? 'pr' : 'issue'} author:${author}`;
+            let total = 0;
+            try {
+              const { data } = await github.rest.search.issuesAndPullRequests({
+                q, per_page: 1,
+              });
+              total = data.total_count;
+            } catch (e) {
+              core.warning(
+                `Search for prior items by ${author} failed: ${e.message}`,
+              );
+              return;
+            }
+            core.info(
+              `Author ${author} has ${total} ${isPR ? 'PR' : 'issue'}(s) ` +
+                `in ${owner}/${repo} (including this one if indexed).`,
+            );
+            if (total > 1) {
+              core.info(`${author} is not a first-time contributor; skipping.`);
+              return;
+            }
+
+            const body = [
+              MARKER,
+              `👋 Thanks for your first contribution to Texera, @${author}!`,
+              ``,
+              `You can drive common housekeeping tasks just by leaving a comment. Type the command on its own line.`,
+              ``,
+              `### On issues`,
+              ``,
+              `| Command | What it does |`,
+              `|---|---|`,
+              `| \`/take\` | Assign the issue to yourself (self-claim it) |`,
+              `| \`/untake\` | Remove yourself as assignee |`,
+              ``,
+              `To find unclaimed work, search \`is:issue is:open no:assignee\` — there's no "triage" label; the search filter *is* the triage state.`,
+              ``,
+              `### Linking sub-issues`,
+              ``,
+              `| Command | Where to run it | What it does |`,
+              `|---|---|---|`,
+              `| \`/sub-issue #12 #13\` | On the **parent** | Links #12 and #13 as children of this issue |`,
+              `| \`/unsub-issue #12 #13\` | On the **parent** | Unlinks those children |`,
+              `| \`/parent-issue #5\` | On the **child** | Sets #5 as this issue's parent |`,
+              `| \`/unparent-issue\` | On the **child** | Removes this issue's parent (auto-detected) |`,
+              `| \`/unparent-issue #5\` | On the **child** | Removes parent #5 explicitly |`,
+              ``,
+              `You can write references as \`#12\` or bare \`12\`. Cross-repo references like \`owner/repo#12\` aren't supported and are ignored.`,
+              ``,
+              `### On pull requests (author only)`,
+              ``,
+              `| Command | What it does |`,
+              `|---|---|`,
+              `| \`/request-review @user [@user ...]\` | Request reviews from those users |`,
+              `| \`/unrequest-review @user [@user ...]\` | Cancel those review requests |`,
+              ``,
+              `You can mention teams as \`@org/team\`, and \`@copilot\` works too. Only the PR **author** can use these commands.`,
+              ``,
+              `> **Note:** Commands must match exactly — \`/take this\` won't work, only \`/take\`. Bots are ignored, and you can't self-link an issue or set an issue as its own parent.`,
+              ``,
+              `For the full contribution flow, see [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md).`,
+            ].join('\n');
+
+            try {
+              await github.rest.issues.createComment({
+                owner, repo, issue_number, body,
+              });
+              core.info(`Posted welcome comment on #${issue_number}`);
+            } catch (e) {
+              core.warning(
+                `Failed to post welcome on #${issue_number}: ${e.message}`,
+              );
+            }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f31b0052e0..8abbf4fc8f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -96,6 +96,46 @@ yarn format:fix
 
 ---
 
+## 👋 Comment commands
+
+You can drive common housekeeping tasks just by leaving a comment on an issue or pull request. Type the command on its own line.
+
+### On issues
+
+| Command | What it does |
+|---|---|
+| `/take` | Assign the issue to yourself (self-claim it) |
+| `/untake` | Remove yourself as assignee |
+
+To find unclaimed work, search `is:issue is:open no:assignee` — there's no "triage" label; the search filter *is* the triage state.
+
+### Linking sub-issues
+
+You can link from either end of the parent/child relationship:
+
+| Command | Where to run it | What it does |
+|---|---|---|
+| `/sub-issue #12 #13` | On the **parent** | Links #12 and #13 as children of this issue |
+| `/unsub-issue #12 #13` | On the **parent** | Unlinks those children |
+| `/parent-issue #5` | On the **child** | Sets #5 as this issue's parent |
+| `/unparent-issue` | On the **child** | Removes this issue's parent (auto-detected) |
+| `/unparent-issue #5` | On the **child** | Removes parent #5 explicitly |
+
+You can write references as `#12` or bare `12`. Cross-repo references like `owner/repo#12` aren't supported and are ignored.
+
+### On pull requests (author only)
+
+| Command | What it does |
+|---|---|
+| `/request-review @user [@user ...]` | Request reviews from those users |
+| `/unrequest-review @user [@user ...]` | Cancel those review requests |
+
+You can mention teams as `@org/team`, and `@copilot` works too. Only the PR **author** can use these commands.
+
+> **Note:** Commands must match exactly — `/take this` won't work, only `/take`. Bots are ignored, and you can't self-link an issue or set an issue as its own parent.
+
+---
+
 ## 📝 Apache License Header
 
 All new files must include the Apache License header.